diff --git a/.gitignore b/.gitignore index b466d32d..9d2389ee 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,6 @@ node_modules .env.production.local .env deploy.env -.vscode npm-debug.log* yarn-debug.log* diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..1d08adfc --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "dbaeumer.vscode-eslint", + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "Prisma.prisma", + "inlang.vs-code-extension" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..d717a2cd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,38 @@ +{ + "editor.tabSize": 2, + "editor.insertSpaces": true, + "editor.formatOnSave": true, + "eslint.enable": true, + "eslint.lintTask.enable": true, + "eslint.lintTask.options": "--ext .js,.jsx,.ts,.tsx,.graphql .", + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact" + ], + "editor.codeActionsOnSave": { + "source.fixAll": true + }, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + "[markdown]": { + "files.trimTrailingWhitespace": false, + "editor.wordWrap": "off", + "editor.formatOnSave": false, + "editor.formatOnPaste": false, + "editor.formatOnType": false + }, + "[prisma]": { + "editor.defaultFormatter": "Prisma.prisma" + }, + "json.schemas": [ + { + "url": "https://cdn.jsdelivr.net/npm/tsup/schema.json", + "fileMatch": [ + "package.json", + "tsup.config.json" + ] + } + ] +} diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index d696decd..c8bcb7a7 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index c46eaa05..c09b7401 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -42,7 +42,6 @@ async function main() { })); app.disable('x-powered-by'); - app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(sessionParser); diff --git a/apps/frontend/src/components/SigninMenu.tsx b/apps/frontend/src/components/SigninMenu.tsx index 3f249d1e..509139e3 100644 --- a/apps/frontend/src/components/SigninMenu.tsx +++ b/apps/frontend/src/components/SigninMenu.tsx @@ -1,7 +1,7 @@ import { IconButton, Menu, MenuItem } from "@mui/material"; import Button from "@mui/material/Button"; import * as React from "react"; -import { Trans } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router"; import { trpc, UserMe } from "~utils/trpc"; @@ -12,6 +12,8 @@ export const SigninMenu = ({ user }: { user: UserMe }) => { const navigate = useNavigate(); const location = useLocation(); + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -80,7 +82,7 @@ export const SigninMenu = ({ user }: { user: UserMe }) => { fontSize: 13, }} > - + {t("menu.signup")} )} @@ -133,20 +135,20 @@ export const SigninMenu = ({ user }: { user: UserMe }) => { > {user && user.role == "Admin" ? ( - + {t("menu.admin")} ) : null} - + {t("menu.profile")} - + {t("menu.settings")} - + {t("menu.logout")} diff --git a/apps/frontend/src/components/project/EditProjectDialog.tsx b/apps/frontend/src/components/project/EditProjectDialog.tsx index 8e207f15..92c9406c 100644 --- a/apps/frontend/src/components/project/EditProjectDialog.tsx +++ b/apps/frontend/src/components/project/EditProjectDialog.tsx @@ -1,14 +1,12 @@ import DeleteIcon from "@mui/icons-material/Delete"; +import EditIcon from "@mui/icons-material/Edit"; import { LoadingButton } from "@mui/lab"; import { - Alert, - Autocomplete, Box, - Button, - Chip, - DialogActions, + FormControlLabel, + FormGroup, + FormHelperText, Grid, - Stack, Switch, Typography, } from "@mui/material"; @@ -17,12 +15,11 @@ import { useFormik } from "formik"; import { useConfirm } from "material-ui-confirm"; import { useSnackbar } from "notistack"; import { Trans, useTranslation } from "react-i18next"; -import { useLocation, useNavigate, useParams } from "react-router"; +import { useNavigate } from "react-router"; import * as Yup from "yup"; import { AutoCompleteTags } from "~components/AutoComleteTags"; import { StyledDialog } from "~components/Dialog"; -import { ERR_ALREADY_EXISTING_PROJECT } from "~utils/Constants"; import { humanizeError } from "~utils/errors"; import { ProjectById, trpc } from "~utils/trpc"; @@ -40,13 +37,30 @@ export const EditProjectDialog: React.FC = ({ const { t } = useTranslation(); const confirm = useConfirm(); const utils = trpc.useUtils(); + const navigate = useNavigate(); const { enqueueSnackbar } = useSnackbar(); + const deleteMutation = trpc.project.delete.useMutation({ + onSuccess: () => { + utils.project.list.invalidate(); + navigate("/"); + }, + onError: (e) => { + console.log(e); + enqueueSnackbar( + t("project.delete.error", "Project n'a pas pu être supprimé", { + variant: "error", + key: "project.delete.error", + }) + ); + }, + }); + const handleDelete = () => { - // deleteMutation.mutate({ - // projectId: project.id, - // }); + deleteMutation.mutate({ + projectId: project.id, + }); }; const confirmDelete = () => { @@ -60,24 +74,24 @@ export const EditProjectDialog: React.FC = ({ color: "error", }, }).then(() => { - // handleDelete(); + handleDelete(); }); }; const mutation = trpc.project.update.useMutation({ onSuccess: () => { utils.project.byId.invalidate({ id: project.id }); - enqueueSnackbar(t("project.edit.success", "Project a été mise à jour"), { + enqueueSnackbar(t("project.edit.success"), { variant: "success", key: "project.edit.success", }); }, onError: (e) => { console.log(e); - enqueueSnackbar( - t("project.edit.error", "Project n'a pas été mise à jour"), - { variant: "error", key: "project.edit.error" } - ); + enqueueSnackbar(t("project.edit.error"), { + variant: "error", + key: "project.edit.error", + }); }, }); @@ -98,39 +112,32 @@ export const EditProjectDialog: React.FC = ({ public: project.public, collaborative: project.collaborative, shared: project.shared, - keywords: [], + keywords: project.keywords, error: null, }, validationSchema: validationSchema, onSubmit: async (values) => { try { - const project = await mutation.mutateAsync({ + await mutation.mutateAsync({ + projectId: project.id, title: values.title, description: values.description, public: values.public, collaborative: values.collaborative, shared: values.shared, + keywords: values.keywords, }); - if (project) { - formik.resetForm(); - onClose(); - } + formik.resetForm(); + onClose(); } catch (e) { - if (e.message == ERR_ALREADY_EXISTING_PROJECT) { - formik.setFieldError( - "title", - humanizeError("ERR_ALREADY_EXISTING_PROJECT") - ); - } else { - formik.setFieldError("title", humanizeError("ERR_UNKOWN")); - } + formik.setFieldError("title", humanizeError("ERR_UNKOWN")); } }, }); return ( = ({ loading={formik.isSubmitting} >
- - - { - formik.setFieldValue("keywords", newValue); - }} - disabled={formik.isSubmitting} - value={formik.values.keywords} - textFieldProps={{ - margin: "normal", - inputProps: { - "data-testid": "keywords", - }, - label: t("project.keywords"), - }} - limitTags={10} - /> + + + + { + formik.setFieldValue("keywords", newValue); + }} + disabled={formik.isSubmitting} + value={formik.values.keywords} + textFieldProps={{ + margin: "normal", + inputProps: { + "data-testid": "keywords", + }, + label: t("project.keywords"), + }} + limitTags={10} + /> - - - - - - - - - - - { - formik.setFieldValue("public", value); - }} - /> - - - - - - - + + {t("project.visibilitySection")} + - - - - - - - - { - formik.setFieldValue("collaborative", value); - }} - /> - - - - - - - + + + { + formik.setFieldValue("public", value); + }} + /> + } + label={t("project.public")} + /> + {t("project.publicHelper")} + - - - - - - - - { - formik.setFieldValue("shared", value); - }} - /> - - - - - - - + + { + formik.setFieldValue("collaborative", value); + }} + /> + } + label={t("project.collaborative")} + /> + + {t("project.collaborativeHelper")} + + - - - Modifier le projet - + + { + formik.setFieldValue("shared", value); + }} + /> + } + label={t("project.shared")} + /> + {t("project.shareHelper")} + + + + + + {project.deletable && ( + } + > + {t("project.edit.delete.button")} + + )} + - {project.deletable && ( - - - {t("deleteAction")} - - )} - + + } + disabled={mutation.isLoading} + > + {t("project.edit.submit.button")} + + + +
); diff --git a/apps/frontend/src/components/project/ExportPanel.tsx b/apps/frontend/src/components/project/ExportPanel.tsx index 7dce0012..f9d3d1d3 100644 --- a/apps/frontend/src/components/project/ExportPanel.tsx +++ b/apps/frontend/src/components/project/ExportPanel.tsx @@ -28,7 +28,7 @@ export const ExportPanel: React.FC = ({ project }: Props) => { }); saveAs(blob, `export.${format}`); - enqueueSnackbar(t("project.export.success", "Votre export est prêt"), { + enqueueSnackbar(t("project.export.success"), { variant: "success", key: "project.export.success", }); diff --git a/apps/frontend/src/components/project/MemberListPanel.tsx b/apps/frontend/src/components/project/MemberListPanel.tsx new file mode 100644 index 00000000..164f9410 --- /dev/null +++ b/apps/frontend/src/components/project/MemberListPanel.tsx @@ -0,0 +1,91 @@ +import { + Box, + Button, + Chip, + List, + ListItem, + ListItemAvatar, + ListItemText, + Paper, + Stack, + Typography, +} from "@mui/material"; +import * as React from "react"; +import { useTranslation } from "react-i18next"; + +import { Avatar } from "~components/Avatar"; +import { TransUserRole } from "~components/TransUserRole"; +import { ProjectById, ProjectMembers, UserMe } from "~utils/trpc"; + +interface SideBarProps { + project: ProjectById; + user?: UserMe; +} + +export const MemberListPanel: React.FC = ({ project, user }) => { + const { t } = useTranslation(); + + return ( + + + {t("project.members", { count: project._count.members })} + + + + + + {project.user.initial} + + + } + /> + + + {project.members.map((member: ProjectMembers) => ( + + + + {member.user?.initial} + + + } + /> + + ))} + + + ); +}; diff --git a/apps/frontend/src/components/project/ProjectEditPanel.tsx b/apps/frontend/src/components/project/ProjectEditPanel.tsx index 13edc2f2..3952e17c 100644 --- a/apps/frontend/src/components/project/ProjectEditPanel.tsx +++ b/apps/frontend/src/components/project/ProjectEditPanel.tsx @@ -1,19 +1,11 @@ -import ContentCopyIcon from "@mui/icons-material/ContentCopy"; -import DeleteIcon from "@mui/icons-material/Delete"; -import { LoadingButton } from "@mui/lab"; -import { Box, Divider, IconButton, Paper, Typography } from "@mui/material"; -import copy from "copy-to-clipboard"; -import { useConfirm } from "material-ui-confirm"; -import { useSnackbar } from "notistack"; +import EditIcon from "@mui/icons-material/Edit"; +import { Button, Paper } from "@mui/material"; import * as React from "react"; -import { Trans, useTranslation } from "react-i18next"; -import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; -import LabeledProgressSwitch from "~components/LabeledProgressSwitch"; -import { ProjectById, trpc, UserMe } from "~utils/trpc"; +import { ProjectById, UserMe } from "~utils/trpc"; import { EditProjectDialog } from "./EditProjectDialog"; -import { ShareDialog } from "./ShareDialog"; interface ProjectEditPanelProps { project: ProjectById; user: UserMe; @@ -24,73 +16,11 @@ export const ProjectEditPanel: React.FC = ({ user, }) => { const { t } = useTranslation(); - const confirm = useConfirm(); - const { enqueueSnackbar } = useSnackbar(); - const navigate = useNavigate(); + const [open, setOpen] = React.useState(false); - const utils = trpc.useUtils(); - - const updateMutation = trpc.project.update.useMutation({ - onSuccess: () => { - utils.project.byId.invalidate({ id: project.id }); - enqueueSnackbar(t("project.edit.success", "Project a été mise à jour"), { - variant: "success", - key: "project.edit.success", - }); - }, - onError: (e) => { - console.log(e); - enqueueSnackbar( - t("project.edit.error", "Project n'a pas été mise à jour"), - { variant: "error", key: "project.edit.error" } - ); - }, - }); - - const deleteMutation = trpc.project.delete.useMutation({ - onSuccess: () => { - utils.project.list.invalidate(); - navigate("/"); - }, - onError: (e) => { - console.log(e); - enqueueSnackbar( - t("project.delete.error", "Project n'a pas pu être supprimé", { - variant: "error", - key: "project.delete.error", - }) - ); - }, - }); - - const handleDelete = () => { - deleteMutation.mutate({ - projectId: project.id, - }); - }; - - const confirmDelete = () => { - confirm({ - title: t("project.confirm-delete.title", "Delete project"), - description: t("project.confirm-delete.description", "Are you sure ?"), - confirmationText: t("deleteAction"), - cancellationText: t("cancelAction"), - confirmationButtonProps: { - variant: "contained", - color: "error", - }, - }).then(() => { - handleDelete(); - }); - }; - - const handleCopyUrl = (e, text: string) => { - e.stopPropagation(); - copy(text); - enqueueSnackbar(t("project.sharecode.copied", "Code du projet copié"), { - variant: "success", - }); - }; + if (project.userId != user.id) { + return null; + } return ( = ({ paddingY: 3, }} > - {user && project.userId == user.id ? ( - <> - - {t("project.edit", "Modification")} - - {/* */} - - updateMutation.mutate({ - projectId: project.id, - public: !project.public, - }) - } - /> - - updateMutation.mutate({ - projectId: project.id, - collaborative: !project.collaborative, - }) - } - /> - - updateMutation.mutate({ - projectId: project.id, - shared: !project.shared, - }) - } - /> - - {project.shared && ( - - - - - - - {project.shareCode} - handleCopyUrl(e, `${project.shareCode}`)} - > - - - - - - {t("project.share.dialog.description")} - - {t("project.share.dialog.linkText")} - - - - )} - - ) : null} + setOpen(false)} + /> + + {/* */} {/*{((user && !isOwner(project, user)) && (user && !isMember(project, user)) && (user && !isAdmin(user)) && project.shared) && @@ -194,7 +59,7 @@ export const ProjectEditPanel: React.FC = ({ {`rejoindre`} - } */} +*/} ); }; diff --git a/apps/frontend/src/components/project/ProjectSummary.tsx b/apps/frontend/src/components/project/ProjectSummary.tsx index 2bbdc8c8..718fde41 100644 --- a/apps/frontend/src/components/project/ProjectSummary.tsx +++ b/apps/frontend/src/components/project/ProjectSummary.tsx @@ -18,9 +18,7 @@ const ProjectSummary: React.FC = ({ project }: Props) => { {project && project.playlist ? ( - - Liste de lecture :{" "} - + {t("project.summary.playlist.title")} {project.playlist.title} ) : null} diff --git a/apps/frontend/src/components/project/SharePanel.tsx b/apps/frontend/src/components/project/SharePanel.tsx new file mode 100644 index 00000000..fb5be067 --- /dev/null +++ b/apps/frontend/src/components/project/SharePanel.tsx @@ -0,0 +1,100 @@ +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import { Box, IconButton, Paper, Typography } from "@mui/material"; +import copy from "copy-to-clipboard"; +import { useSnackbar } from "notistack"; +import * as React from "react"; +import { Trans, useTranslation } from "react-i18next"; + +import { ProjectById, UserMe } from "~utils/trpc"; + +import { ShareDialog } from "./ShareDialog"; +interface ProjectEditPanelProps { + project: ProjectById; + user: UserMe; +} + +export const SharePanel: React.FC = ({ + project, + user, +}) => { + const { t } = useTranslation(); + const { enqueueSnackbar } = useSnackbar(); + + const handleCopyUrl = (e, text: string) => { + e.stopPropagation(); + copy(text); + enqueueSnackbar(t("project.sharecode.copied", "Code du projet copié"), { + variant: "success", + }); + }; + + if (!project.shared || (user && project.userId != user.id)) { + return null; + } + + return ( + + + {t("project.share.section.title")} + + + + + + + + + {project.shareCode} + handleCopyUrl(e, `${project.shareCode}`)} + > + + + + + + {t("project.share.dialog.description")} + + {t("project.share.dialog.linkText")} + + + + ) + {/*{((user && !isOwner(project, user)) && (user && !isMember(project, user)) + && (user && !isAdmin(user)) && project.shared) && +
+ + {`rejoindre`} + +
+ } */} +
+ ); +}; diff --git a/apps/frontend/src/components/project/SideBar.tsx b/apps/frontend/src/components/project/SideBar.tsx index cecfea36..4bca5e1c 100644 --- a/apps/frontend/src/components/project/SideBar.tsx +++ b/apps/frontend/src/components/project/SideBar.tsx @@ -18,8 +18,10 @@ import { TransUserRole } from "~components/TransUserRole"; import { ProjectById, ProjectMembers, UserMe } from "~utils/trpc"; import { ExportPanel } from "./ExportPanel"; +import { MemberListPanel } from "./MemberListPanel"; import { PlaylistSideBar } from "./PlaylistSideBar"; import { ProjectEditPanel } from "./ProjectEditPanel"; +import { SharePanel } from "./SharePanel"; interface SideBarProps { project: ProjectById; @@ -27,95 +29,17 @@ interface SideBarProps { } export const SideBar: React.FC = ({ project, user }) => { - const { t } = useTranslation(); - return ( + {user ? : null} - - {user && } - - {user && project.userId == user.id ? ( - - {/*{((user && !isOwner(project, user)) && (user && !isMember(project, user)) - && (user && !isAdmin(user)) && project.shared) && -
- - {`rejoindre`} - -
- } */} - - - {t("project.members", { count: project._count.members })} - - - - - - {project.user.initial} - - - } - /> - - - {project.members.map((member: ProjectMembers) => ( - - - - {member.user?.initial} - - - } - /> - - ))} - -
+ {user ? ( + + + + + ) : null} - - {user && }
); }; diff --git a/apps/frontend/src/locales/en/common.json b/apps/frontend/src/locales/en/common.json index 215d101f..2edef048 100644 --- a/apps/frontend/src/locales/en/common.json +++ b/apps/frontend/src/locales/en/common.json @@ -1 +1 @@ -{"about.intro":"The development of <0>Celluloid is led by Michaël Bourgatte and Laurent Tessier within a multidisciplinary <1>Huma-Num Consortium gathering researchers mobilizing audiovisual corpora. ","about.opensource.github":"Celluloid is an Open Source project in Digital Humanities. The code and the instructions for use are freely available on GitHub.","about.opensource.prefix":"The version 2.0, \"Alphaville\", as well as its successive developments, are realized within the framework of the consortium Huma-Num <0>CANEVAS (within the <1>Maison des Sciences de l'Homme Paris Nord). This version is developed by Younès Benomar.","about.support":"Version 1.0 of Celluloid, \"Pierrot\", as well as its successive developments, were supported by the Catholic University of Paris, Saint Matthieu Foundation and La Paillasse projects incubator. This version was developed by Erwan Queffélec with the participation of Jean-Frédéric Bruzek, Souleymane Thiam and Guillaume Aichhorn.","about.title":"About","annotation.comment.cancel":"Cancel","annotation.comment.reply":"Reply","annotation.comment.send":"Send","annotation.commentLabel":"{{count}} comment","annotation.commentLabel_one":"one comment","annotation.commentLabel_other":"{{count}} comments","annotation.commentLabel_plural":"{{count}} comments","annotation.commentPlaceholder":"Leave a comment…","annotation.confirm-delete.description":"Are you sure you want to delete the annotation?","annotation.confirm-delete.title":"Delete annotation","annotation.contentPlaceholder":"type in your annotation…","annotation.create.cancel":"Cancel","annotation.create.send":"Send","annotation.hintLabel":"{{count}} annotation","annotation.hintLabel_many":"{{count}} annotations","annotation.hintLabel_one":"{{count}} annotation","annotation.hintLabel_other":"{{count}} annotations","annotation.hintLabel_plural":"{{count}} annotations","annotation.hintLabelNone":"No annotation at this time","annotation.pauseLabel":"Pause video ?","cancelAction":"Cancel","createAction":"Save","deleteAction":"Delete","ERR_ALREADY_EXISTING_PROJECT":"A project with the same title already exists, please rename the project","ERR_UNKOWN":"An unknown error has occurred. Please try again later or contact support for assistance.","footer.copyright":"<0>CC BY-NC 2023 Consortium Canevas","footer.legalNotice":"Legal notice","footer.termsAndConditions":"Terms & Conditions","home.addVideo":"Add a link to a PeerTube video...","home.description":"A webservice to annotate, comment and analyze audiovisual content (movies, series, TV shows or your own videos – interviews, ethnographic documentary…), alone or as a team","home.emptySearchResult":"No matching projects","home.joinProject":"join a project","home.myProjects":"My projects","home.newProject":"Create project","home.publicProjects":"Explore","home.searchProject":"Type anything…","home.title":"What is Celluloid ?","home.tutoriel.description":"Go to <0>Peertube\nChoose a video or upload a new one\nCopy the link to share the video\nCreate your project in Celluloid, copy the video link and go!\nWant to work with others? Share the project code with your partners","home.tutoriel.link":"The tutorial is here: <0>https://celluloid.hypotheses.org/1365","home.tutoriel.prefix":"To discover the application, you can consult the ","home.tutoriel.subtitle":"You want to know more or you encounter difficulties?","home.tutoriel.title":"How to use Celluloid ?","Les annotations correspondant aux différents temps de la vidéo seront affichées ici.":"Les annotations correspondant aux différents temps de la vidéo seront affichées ici.","menu.about":"about","menu.admin":"Admin","menu.create":"Create","menu.explore":"Explore","menu.join":"Join","menu.login":"login","menu.logout":"Logout","menu.profile":"Profile","menu.signup":"signup","notFound.action":"back to home","notFound.description":"The page you are looking for might be private or may have been deleted","notFound.title":"Page not found :(","printAction":"Print","profile.me.project.empty":"You have no projects.","project.add-related-video":"Playlist","project.add-video-playlist-description":"Please enter the URL of the video you would like to add to the playlist.","project.add-video-to-playlist-button":"Add a video to the playlist.","project.annotation.hints.label":"Display the timeline of annotations.","project.annotation.title":"Annotations","project.assignment":"Task","project.assignmentPlaceholder":"Add a task","project.assignmentsHelper":"List the tasks you submitt to the attendees","project.assignmentsSection":"Tasks","project.cancel-add-video":"Add","project.cancelAction":"Cancel","project.codeWarning.description":"This code will be available on the project page.\n To reset it, just reshare the project.","project.codeWarning.title":"Share this code with the participants","project.collaborative":"Collaborative","project.collaborativeHelper":"Attendees to a collaborative project will be allowed to annotate or comment the video. If your project is not collaborative, only you can annotate and comment on it.","project.confirm-delete.description":"Are you sure ?","project.confirm-delete.title":"Delete project","project.create.error.video-info-failed":"Failed to retrieve video information. Please check the URL and try again.","project.create.url.not-valid":"The provided URL is not valid. Please check and try again.","project.create.url.required":"URL is required. Please provide a valid URL.","project.createAction":"Create project","project.createTitle":"New project","project.creatorRole":"Creator","project.description":"Description","project.descriptionHelper":"Briefly describe your video","project.edit":"Edit","project.edit.error":"Project could not be deleted.","project.edit.success":"Project has been updated.","project.export":"Export Annotations","project.keywords":"keywords","project.members":"{{ count }} attendees","project.members_many":"{{ count }} attendees","project.members_one":"an attendant","project.members_other":"{{ count }} attendees","project.members_plural":"{{ count }} attendees","project.objective":"Objective","project.objectiveHelper":"Choose an objective","project.playlist":"Playlist","project.public":"Public","project.publicHelper":"A public project will be visible by all users, even logged-out. However, they won't be able to see the annotations and comments, nor to add their own.","project.share.dialog.description":"To open printable instructions in a new widow ","project.share.dialog.linkText":"click here","project.share.guide.step1":"Open the web page","project.share.guide.step2":"On the landing page, click 'JOIN PROJECT'","project.share.guide.step3":"Enter the project code","project.share.guide.step4":"Enter your username and an answer to a secret question","project.share.guide.step5":"Make sure to read carefully the objective and assignments","project.share.guide.step6":"Complete the assignments and annotate the video while it's playing","project.share.guide.subtitle":"How to use Celluloid ?","project.share.guide.title":"Instructions","project.shared":"Share","project.submit-add-video-playlist":"Cancel","project.summary.playlist.title":"Playlist","project.title":"Title","project.titleHelper":"Choose a meaningful title for your project","project.URL_title":"Video URL","project.videoUrlHelper":"Link to the original video","project.visibilitySection":"Visibility","search.placeholder":"Search projet","shareAction":"Share","signin.alreadyRegistered":"Already registered?","signin.changePasswordAction":"reset password","signin.code":"Confirmation code","signin.codeHelper":"This code was sent to you by email","signin.confirmPassword":"Confirm password","signin.confirmSignupAction":"confirm signup","signin.confirmSignupTitle":"Confirm Signup","signin.email":"Email address","signin.forgotPasswordAction":"forgot password","signin.forgotPasswordTitle":"Forgot password","signin.joinAction":"join","signin.joinProjectTitle":"Join project","signin.lastName":"Last name","signin.login":"Email or username","signin.loginAction":"Login","signin.loginTitle":"Login","signin.notRegistered":"Not registered?","signin.password":"Password","signin.passwordHelper":"Minimum 8 characters","signin.passwordMismatch":"Confirmation doesn't match password","signin.projectCode":"Project code","signin.rememberlastName":"Your last name will serve as your login password","signin.resendCodeAction":"resend code","signin.resetAction":"reset","signin.signupAction":"signup","signin.signupOrLoginMessage":"Please signup or login to continue","signin.signupTitle":"Signup","signin.upgradeAccountMessage":"Please enter a valid email and a password to continue","signin.username":"Fisrtname or Username","tagSearch.createLabel":"Create tag","tagSearch.prefix":"Tag","update.action":"Refresh","update.message":"This app was just updated! Please refresh the page.","home.teachers":"Teacher","home.students":"Student","annotation.form.add-annotation":"Add annotation","annotation.edit.send":"Edit","confirm.username.required":"The username is required.","confirm.code.required":"The password is required.","confirm.title":"Confirmation","confirm.username.label":"Email or username","confirm.username.paceholder":"Email or username","confirm.code.label":"Confirmation code","confirm.code.placeholder":"Confirmation code","confirm.button.submit":"Send","forgot.email.required":"The Email is required","forgot.title":"Lost password","forgot.email.placeholder":"Email or username","forgot.button.recover":"Recover account","forgot.button.submit":"Change password","join.error.project-not-found":"Invalid project share code","join.title":"Join project","join.shareCode.label":"Project code","join.shareCode.placeholder":"Project code","join.button.submit":"Join","signin.username.required":"Username required","signin.password.required":"Password required","signin.title":"Signin","signin.confirm.button":"Confirm","recover.username.required":"Username is required","recover.code.required":"Code is required","recover.password.required":"Password is required","recover.title":"Recover account","recover.username.label":"Email or username","recover.username.paceholder":"Email or username","recover.code.label":"Confirmation code","recover.code.placeholder":"Confirmation code","recover.password.label":"Password","recover.password.placeholder":"Password","recover.passwordConfirmation.label":"Password confirmation","recover.passwordConfirmation.placeholder":"Password confirmation","recover.button.submit":"Send","signup.error.account_exists":"This email address is already registered.","signup.title":"Signin","signup.username.label":"Username","signup.username.paceholder":"Username","signup.email.label":"Email","signup.email.placeholder":"Email","signup.password.label":"Password","signup.password.placeholder":"Password","signup.passwordConfirmation.label":"Password confirmation","signup.passwordConfirmation.placeholder":"Password confirmation","signup.button.submit":"Signin","student-student-signup.error.username-exists":"Email exists","student-signup.title":"Registration","student-student-signup.shareCode.label":"Project code","student-signup.shareCode.placeholder":"Project code","student-student-signup.username.label":"Name or username","student-student-signup.username.paceholder":"Name or username","student-signup.password.label":"Password","student-signup.password.placeholder":"Password","student-signup.login.button":"Login","student-signup.button.submit":"Join","project.delete.error":"Project deleted","project.sharecode.copied":"Project code copied","home.projects.retry":"Retry","password.unmatch":"The password does not match.","profile.role.student":"Student","profile.role.teacher":"Teacher","profile.role.admin":"Admin","join.error.project-owner-cannot-join":"The project owner cannot join the project as a member.","project.annotaions.empty":"Les annotations correspondant aux différents temps de la vidéo seront\n affichées ici.","user.update.success":"\"Profile updated\""} \ No newline at end of file +{"about.intro":"The development of <0>Celluloid is led by Michaël Bourgatte and Laurent Tessier within a multidisciplinary <1>Huma-Num Consortium gathering researchers mobilizing audiovisual corpora. ","about.opensource.github":"Celluloid is an Open Source project in Digital Humanities. The code and the instructions for use are freely available on GitHub.","about.opensource.prefix":"The version 2.0, \"Alphaville\", as well as its successive developments, are realized within the framework of the consortium Huma-Num <0>CANEVAS (within the <1>Maison des Sciences de l'Homme Paris Nord). This version is developed by Younès Benomar.","about.support":"Version 1.0 of Celluloid, \"Pierrot\", as well as its successive developments, were supported by the Catholic University of Paris, Saint Matthieu Foundation and La Paillasse projects incubator. This version was developed by Erwan Queffélec with the participation of Jean-Frédéric Bruzek, Souleymane Thiam and Guillaume Aichhorn.","about.title":"About","annotation.comment.cancel":"Cancel","annotation.comment.reply":"Reply","annotation.comment.send":"Send","annotation.commentLabel":"{{count}} comment","annotation.commentLabel_one":"one comment","annotation.commentLabel_other":"{{count}} comments","annotation.commentLabel_plural":"{{count}} comments","annotation.commentPlaceholder":"Leave a comment…","annotation.confirm-delete.description":"Are you sure you want to delete the annotation?","annotation.confirm-delete.title":"Delete annotation","annotation.contentPlaceholder":"type in your annotation…","annotation.create.cancel":"Cancel","annotation.create.send":"Send","annotation.hintLabel":"{{count}} annotation","annotation.hintLabel_many":"{{count}} annotations","annotation.hintLabel_one":"{{count}} annotation","annotation.hintLabel_other":"{{count}} annotations","annotation.hintLabel_plural":"{{count}} annotations","annotation.hintLabelNone":"No annotation at this time","annotation.pauseLabel":"Pause video ?","cancelAction":"Cancel","createAction":"Save","deleteAction":"Delete","ERR_ALREADY_EXISTING_PROJECT":"A project with the same title already exists, please rename the project","ERR_UNKOWN":"An unknown error has occurred. Please try again later or contact support for assistance.","footer.copyright":"<0>CC BY-NC 2023 Consortium Canevas","footer.legalNotice":"Legal notice","footer.termsAndConditions":"Terms & Conditions","home.addVideo":"Add a link to a PeerTube video...","home.description":"A webservice to annotate, comment and analyze audiovisual content (movies, series, TV shows or your own videos – interviews, ethnographic documentary…), alone or as a team","home.emptySearchResult":"No matching projects","home.joinProject":"join a project","home.myProjects":"My projects","home.newProject":"Create project","home.publicProjects":"Explore","home.searchProject":"Type anything…","home.title":"What is Celluloid ?","home.tutoriel.description":"Go to <0>Peertube\nChoose a video or upload a new one\nCopy the link to share the video\nCreate your project in Celluloid, copy the video link and go!\nWant to work with others? Share the project code with your partners","home.tutoriel.link":"The tutorial is here: <0>https://celluloid.hypotheses.org/1365","home.tutoriel.prefix":"To discover the application, you can consult the ","home.tutoriel.subtitle":"You want to know more or you encounter difficulties?","home.tutoriel.title":"How to use Celluloid ?","Les annotations correspondant aux différents temps de la vidéo seront affichées ici.":"Les annotations correspondant aux différents temps de la vidéo seront affichées ici.","menu.about":"about","menu.admin":"Admin","menu.create":"Create","menu.explore":"Explore","menu.join":"Join","menu.login":"login","menu.logout":"Logout","menu.profile":"Profile","menu.signup":"signup","notFound.action":"back to home","notFound.description":"The page you are looking for might be private or may have been deleted","notFound.title":"Page not found :(","printAction":"Print","profile.me.project.empty":"You have no projects.","project.add-related-video":"Playlist","project.add-video-playlist-description":"Please enter the URL of the video you would like to add to the playlist.","project.add-video-to-playlist-button":"Add a video to the playlist.","project.annotation.hints.label":"Display the timeline of annotations.","project.annotation.title":"Annotations","project.assignment":"Task","project.assignmentPlaceholder":"Add a task","project.assignmentsHelper":"List the tasks you submitt to the attendees","project.assignmentsSection":"Tasks","project.cancel-add-video":"Add","project.cancelAction":"Cancel","project.codeWarning.description":"This code will be available on the project page.\n To reset it, just reshare the project.","project.codeWarning.title":"Share this code with the participants","project.collaborative":"Collaborative","project.collaborativeHelper":"Attendees to a collaborative project will be allowed to annotate or comment the video. If your project is not collaborative, only you can annotate and comment on it.","project.confirm-delete.description":"Are you sure ?","project.confirm-delete.title":"Delete project","project.create.error.video-info-failed":"Failed to retrieve video information. Please check the URL and try again.","project.create.url.not-valid":"The provided URL is not valid. Please check and try again.","project.create.url.required":"URL is required. Please provide a valid URL.","project.createAction":"Create project","project.createTitle":"New project","project.creatorRole":"Creator","project.description":"Description","project.descriptionHelper":"Briefly describe your video","project.edit":"Edit","project.edit.error":"Project could not be deleted.","project.edit.success":"Project has been updated.","project.export":"Export Annotations","project.keywords":"keywords","project.members":"{{ count }} attendees","project.members_many":"{{ count }} attendees","project.members_one":"an attendant","project.members_other":"{{ count }} attendees","project.members_plural":"{{ count }} attendees","project.objective":"Objective","project.objectiveHelper":"Choose an objective","project.playlist":"Playlist","project.public":"Public","project.publicHelper":"A public project will be visible by all users, even logged-out. However, they won't be able to see the annotations and comments, nor to add their own.","project.share.dialog.description":"To open printable instructions in a new widow ","project.share.dialog.linkText":"click here","project.share.guide.step1":"Open the web page","project.share.guide.step2":"On the landing page, click 'JOIN PROJECT'","project.share.guide.step3":"Enter the project code","project.share.guide.step4":"Enter your username and an answer to a secret question","project.share.guide.step5":"Make sure to read carefully the objective and assignments","project.share.guide.step6":"Complete the assignments and annotate the video while it's playing","project.share.guide.subtitle":"How to use Celluloid ?","project.share.guide.title":"Instructions","project.shared":"Share","project.submit-add-video-playlist":"Cancel","project.summary.playlist.title":"Playlist","project.title":"Title","project.titleHelper":"Choose a meaningful title for your project","project.URL_title":"Video URL","project.videoUrlHelper":"Link to the original video","project.visibilitySection":"Visibility","search.placeholder":"Search projet","shareAction":"Share","signin.alreadyRegistered":"Already registered?","signin.changePasswordAction":"reset password","signin.code":"Confirmation code","signin.codeHelper":"This code was sent to you by email","signin.confirmPassword":"Confirm password","signin.confirmSignupAction":"confirm signup","signin.confirmSignupTitle":"Confirm Signup","signin.email":"Email address","signin.forgotPasswordAction":"forgot password","signin.forgotPasswordTitle":"Forgot password","signin.joinAction":"join","signin.joinProjectTitle":"Join project","signin.lastName":"Last name","signin.login":"Email or username","signin.loginAction":"Login","signin.loginTitle":"Login","signin.notRegistered":"Not registered?","signin.password":"Password","signin.passwordHelper":"Minimum 8 characters","signin.passwordMismatch":"Confirmation doesn't match password","signin.projectCode":"Project code","signin.rememberlastName":"Your last name will serve as your login password","signin.resendCodeAction":"resend code","signin.resetAction":"reset","signin.signupAction":"signup","signin.signupOrLoginMessage":"Please signup or login to continue","signin.signupTitle":"Signup","signin.upgradeAccountMessage":"Please enter a valid email and a password to continue","signin.username":"Fisrtname or Username","tagSearch.createLabel":"Create tag","tagSearch.prefix":"Tag","update.action":"Refresh","update.message":"This app was just updated! Please refresh the page.","home.teachers":"Teacher","home.students":"Student","annotation.form.add-annotation":"Add annotation","annotation.edit.send":"Edit","confirm.username.required":"The username is required.","confirm.code.required":"The password is required.","confirm.title":"Confirmation","confirm.username.label":"Email or username","confirm.username.paceholder":"Email or username","confirm.code.label":"Confirmation code","confirm.code.placeholder":"Confirmation code","confirm.button.submit":"Send","forgot.email.required":"The Email is required","forgot.title":"Lost password","forgot.email.placeholder":"Email or username","forgot.button.recover":"Recover account","forgot.button.submit":"Change password","join.error.project-not-found":"Invalid project share code","join.title":"Join project","join.shareCode.label":"Project code","join.shareCode.placeholder":"Project code","join.button.submit":"Join","signin.username.required":"Username required","signin.password.required":"Password required","signin.title":"Signin","signin.confirm.button":"Confirm","recover.username.required":"Username is required","recover.code.required":"Code is required","recover.password.required":"Password is required","recover.title":"Recover account","recover.username.label":"Email or username","recover.username.paceholder":"Email or username","recover.code.label":"Confirmation code","recover.code.placeholder":"Confirmation code","recover.password.label":"Password","recover.password.placeholder":"Password","recover.passwordConfirmation.label":"Password confirmation","recover.passwordConfirmation.placeholder":"Password confirmation","recover.button.submit":"Send","signup.error.account_exists":"This email address is already registered.","signup.title":"Signin","signup.username.label":"Username","signup.username.paceholder":"Username","signup.email.label":"Email","signup.email.placeholder":"Email","signup.password.label":"Password","signup.password.placeholder":"Password","signup.passwordConfirmation.label":"Password confirmation","signup.passwordConfirmation.placeholder":"Password confirmation","signup.button.submit":"Signin","student-student-signup.error.username-exists":"Email exists","student-signup.title":"Registration","student-student-signup.shareCode.label":"Project code","student-signup.shareCode.placeholder":"Project code","student-student-signup.username.label":"Name or username","student-student-signup.username.paceholder":"Name or username","student-signup.password.label":"Password","student-signup.password.placeholder":"Password","student-signup.login.button":"Login","student-signup.button.submit":"Join","project.delete.error":"Project deleted","project.sharecode.copied":"Project code copied","home.projects.retry":"Retry","password.unmatch":"The password does not match.","profile.role.student":"Student","profile.role.teacher":"Teacher","profile.role.admin":"Admin","join.error.project-owner-cannot-join":"The project owner cannot join the project as a member.","project.annotaions.empty":"Les annotations correspondant aux différents temps de la vidéo seront\n affichées ici.","user.update.success":"\"Profile updated\"","edit-project-dialog.title":"\"Edit project\"","project.share.section.title":"\"Share\"","project.edit.button":"Edit project","project.share":"Share","project.shareHelper":"A project code will be generated to allow collaborators to annotate your project.","project.export.success":"\"Annotation successfully exported.\"","project.edit.submit.button":"Edit Project","project.edit.delete.button":"Delete Project","project.edit.dialog.title":"Edit Project","menu.settings":"Settings"} \ No newline at end of file diff --git a/packages/trpc/src/routers/project.ts b/packages/trpc/src/routers/project.ts index 160d6141..e7009fea 100644 --- a/packages/trpc/src/routers/project.ts +++ b/packages/trpc/src/routers/project.ts @@ -261,8 +261,8 @@ export const projectRouter = router({ description: z.string().nullish(), public: z.boolean().nullish(), collaborative: z.boolean().nullish(), - shared: z.boolean().nullish(), - keywords: z.array(z.string()) + shared: z.boolean().default(false), + keywords: z.array(z.string()).default([]) }), ) .mutation(async ({ input, ctx }) => { @@ -282,9 +282,12 @@ export const projectRouter = router({ let shareCode = project.shareCode; - if (project.shared != input.shared && input.shared != null) { + const newTitle = input.title != project.title ? input.title : project.title + + if (project.shared != input.shared) { if (input.shared) { - shareCode = generateUniqueShareName(project.title); + console.log("generate new share code with:", newTitle) + shareCode = generateUniqueShareName(newTitle); } else { shareCode = null; @@ -302,7 +305,8 @@ export const projectRouter = router({ shared: input.shared !== null ? input.shared : false, keywords: input.keywords || project.keywords, shareCode - } + }, + select: defaultProjectSelect }); return updatedProject; diff --git a/packages/utils/package.json b/packages/utils/package.json index 2b663a5b..fa8f8627 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -18,6 +18,7 @@ "@t3-oss/env-core": "^0.7.1", "bcryptjs": "^2.4.3", "change-case": "^4.1.2", + "unique-slug": "^4.0.0", "zod": "^3.22.4" } } diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index 638a65ac..255e7f3b 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -1,26 +1,17 @@ import { paramCase } from "change-case"; - export function generateUniqueShareName(title: string) { - const compare = (a: string, b: string) => b.length - a.length; - - const result: string[] = []; - - const construct = (str: string) => { - if (str && result.join('-').length + str.length + 1 <= 6) { - result.push(str); - } - }; - - paramCase(title) + const result = paramCase(title) .split(/-/) - .sort(compare) - .forEach(construct); - + .sort((a: string, b: string) => b.length - a.length) + .reduce((acc: string[], str: string) => { + if (acc.join('-').length < 6) { + acc.push(str); + } + return acc; + }, []); const prefix = result.join('-'); - // Generate a 4-digit random number const randomNumber = Math.floor(1000 + Math.random() * 9000); - return `${prefix ? prefix + '-' : ''}${randomNumber}`; } diff --git a/yarn.lock b/yarn.lock index fa65b263..5149f6ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1968,6 +1968,7 @@ __metadata: change-case: ^4.1.2 tsup: ^7.2.0 typescript: ^5.2.2 + unique-slug: ^4.0.0 zod: ^3.22.4 languageName: unknown linkType: soft