НMerge branch 'dev' into create-shout-2

This commit is contained in:
bniwredyc 2023-04-30 17:40:42 +02:00
commit 62399ce138
40 changed files with 1141 additions and 394 deletions

View File

@ -0,0 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.5542 12.1407L16.417 15.0035L15.4205 16L11.657 12.2365H9.26153V10.6123H10.0497L8.25384 8.84457H5.0531V7.18383H6.58994L4.80805 5.40195H4.19466C3.50174 5.39979 2.83667 5.6739 2.34637 6.16338C1.85628 6.65308 1.58137 7.31794 1.58278 8.01086C1.58415 8.70357 1.8616 9.36744 2.35366 9.85502C2.83688 10.3504 3.50289 10.6243 4.19466 10.6123H7.60079V12.2365H4.19466C3.07508 12.2532 1.99923 11.801 1.2278 10.9895C0.441236 10.1933 0 9.11922 0 8C0 6.88077 0.4412 5.80674 1.2278 5.01052C1.78565 4.42814 2.50526 4.02601 3.29384 3.85636L0.417108 0.996475L1.41358 0L5.17706 3.7776H5.19669L6.82089 5.4018H6.79831L8.58019 7.18368L10.2551 8.84442L12.0369 10.6263L13.5683 12.1378L13.5542 12.1407ZM15.6203 10.9895C16.4069 10.1933 16.8481 9.11927 16.8481 8.00005C16.8481 6.88083 16.4069 5.80679 15.6203 5.01057C14.8463 4.20416 13.7709 3.75724 12.6535 3.7777H9.26153V5.4019H12.6677H12.6675C13.3604 5.39974 14.0255 5.67385 14.5157 6.16333C15.0058 6.65303 15.2807 7.31789 15.2793 8.01081C15.278 8.70352 15.0005 9.36739 14.5085 9.85497C14.3296 10.0348 14.1269 10.1892 13.9061 10.3138L15.0771 11.4819V11.4821C15.2715 11.3333 15.4533 11.1685 15.6204 10.9895L15.6203 10.9895Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -2,24 +2,22 @@
"...subscribing": "...subscribing", "...subscribing": "...subscribing",
"About myself": "About myself", "About myself": "About myself",
"About the project": "About the project", "About the project": "About the project",
"actions": "actions",
"Add comment": "Comment", "Add comment": "Comment",
"Address on Discourse": "Address on Discourse", "Address on Discourse": "Address on Discourse",
"All": "All", "All": "All",
"All authors": "All authors", "All authors": "All authors",
"All posts": "All posts", "All posts": "All posts",
"all topics": "all topics",
"All topics": "All topics", "All topics": "All topics",
"Almost done! Check your email.": "Almost done! Just checking your email.", "Almost done! Check your email.": "Almost done! Just checking your email.",
"Artworks": "Artworks", "Artworks": "Artworks",
"Audio": "Audio", "Audio": "Audio",
"author": "author",
"Author": "Author", "Author": "Author",
"authors": "authors", "Author subscriptions": "Подписки на авторов",
"Authors": "Authors", "Authors": "Authors",
"Back to main page": "Back to main page", "Back to main page": "Back to main page",
"Become an author": "Become an author", "Become an author": "Become an author",
"Bookmarked": "Saved", "Bookmarked": "Saved",
"Bookmarks": "Bookmarks",
"By alphabet": "By alphabet", "By alphabet": "By alphabet",
"By authors": "By authors", "By authors": "By authors",
"By name": "By name", "By name": "By name",
@ -28,23 +26,23 @@
"By relevance": "By relevance", "By relevance": "By relevance",
"By shouts": "By publications", "By shouts": "By publications",
"By signing up you agree with our": "By signing up you agree with our", "By signing up you agree with our": "By signing up you agree with our",
"By time": "By time",
"By title": "By title", "By title": "By title",
"By updates": "By updates", "By updates": "By updates",
"By views": "By views", "By views": "By views",
"cancel": "Cancel", "Characters": "Знаков",
"Chat Title": "Chat Title", "Chat Title": "Chat Title",
"Choose who you want to write to": "Choose who you want to write to", "Choose who you want to write to": "Choose who you want to write to",
"Collaborate": "Help Edit", "Collaborate": "Help Edit",
"collections": "collections",
"Comments": "Comments", "Comments": "Comments",
"Communities": "Communities", "Communities": "Communities",
"community": "community",
"Cooperate": "Cooperate", "Cooperate": "Cooperate",
"Copy": "Copy", "Copy": "Copy",
"Copy link": "Copy link", "Copy link": "Copy link",
"Create account": "Create an account", "Corrections history": "Corrections history",
"Create Chat": "Create Chat", "Create Chat": "Create Chat",
"Create Group": "Create a group", "Create Group": "Create a group",
"Create account": "Create an account",
"Create post": "Create post", "Create post": "Create post",
"Date of Birth": "Date of Birth", "Date of Birth": "Date of Birth",
"Delete": "Delete", "Delete": "Delete",
@ -52,30 +50,28 @@
"Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects", "Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects": "Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects",
"Discours is created with our common effort": "Discours exists because of our common effort", "Discours is created with our common effort": "Discours exists because of our common effort",
"Discussing": "Discussing", "Discussing": "Discussing",
"discussion": "discourse",
"Discussion rules": "Discussion rules", "Discussion rules": "Discussion rules",
"Dogma": "Dogma", "Dogma": "Dogma",
"Drafts": "Drafts", "Drafts": "Drafts",
"Edit": "Edit", "Edit": "Edit",
"Editing": "Editing",
"Email": "Mail", "Email": "Mail",
"email not confirmed": "email not confirmed",
"enter": "enter",
"Enter": "Enter", "Enter": "Enter",
"Enter URL address": "Enter URL address",
"Enter text": "Enter text", "Enter text": "Enter text",
"Enter the code or click the link from email to confirm": "Enter the code from the email or follow the link in the email to confirm registration",
"Enter the Discours": "Enter the Discours", "Enter the Discours": "Enter the Discours",
"Enter the code or click the link from email to confirm": "Enter the code from the email or follow the link in the email to confirm registration",
"Enter your new password": "Enter your new password", "Enter your new password": "Enter your new password",
"Error": "Error", "Error": "Error",
"Everything is ok, please give us your email address": "It's okay, just enter your email address to receive a password reset link.", "Everything is ok, please give us your email address": "It's okay, just enter your email address to receive a password reset link.",
"FAQ": "Tips and suggestions",
"Favorite": "Favorites", "Favorite": "Favorites",
"Favorite topics": "Favorite topics", "Favorite topics": "Favorite topics",
"feed": "feed",
"Feed settings": "Feed settings", "Feed settings": "Feed settings",
"Feedback": "Feedback", "Feedback": "Feedback",
"Fill email": "Fill email", "Fill email": "Fill email",
"Follow": "Follow", "Follow": "Follow",
"Follow the topic": "Follow the topic", "Follow the topic": "Follow the topic",
"follower": "follower",
"Followers": "Followers", "Followers": "Followers",
"Forgot password?": "Forgot your password?", "Forgot password?": "Forgot your password?",
"Forward": "Forward", "Forward": "Forward",
@ -84,12 +80,16 @@
"Go to main page": "Go to main page", "Go to main page": "Go to main page",
"Group Chat": "Group Chat", "Group Chat": "Group Chat",
"Groups": "Groups", "Groups": "Groups",
"Headers": "Headers",
"Help": "Помощь",
"Help to edit": "Help to edit", "Help to edit": "Help to edit",
"Here you can customize your profile the way you want.": "Here you can customize your profile the way you want.", "Here you can customize your profile the way you want.": "Here you can customize your profile the way you want.",
"Hooray! Welcome!": "Hooray! Welcome!", "Hooray! Welcome!": "Hooray! Welcome!",
"Horizontal collaborative journalistic platform": "Horizontal collaborative journalism platform", "Horizontal collaborative journalistic platform": "Horizontal collaborative journalism platform",
"Hotkeys": "Горячие клавиши",
"How can I help/skills": "How can I help/skills", "How can I help/skills": "How can I help/skills",
"How it works": "How it works", "How it works": "How it works",
"How to write a good article": "Как написать хорошую статью",
"How to write an article": "How to write an article", "How to write an article": "How to write an article",
"I have an account": "I have an account!", "I have an account": "I have an account!",
"I have no account yet": "I don't have an account yet", "I have no account yet": "I don't have an account yet",
@ -97,7 +97,8 @@
"Independant magazine with an open horizontal cooperation about culture, science and society": "Independant magazine with an open horizontal cooperation about culture, science and society", "Independant magazine with an open horizontal cooperation about culture, science and society": "Independant magazine with an open horizontal cooperation about culture, science and society",
"Introduce": "Introduction", "Introduce": "Introduction",
"Invalid email": "Check if your email is correct", "Invalid email": "Check if your email is correct",
"invalid password": "invalid password", "Invalid url format": "Invalid url format",
"Invite co-authors": "Invite co-authors",
"Invite to collab": "Invite to Collab", "Invite to collab": "Invite to Collab",
"It does not look like url": "It doesn't look like a link", "It does not look like url": "It doesn't look like a link",
"Join": "Join", "Join": "Join",
@ -106,10 +107,13 @@
"Join the global community of authors!": "Join the global community of authors from all over the world!", "Join the global community of authors!": "Join the global community of authors from all over the world!",
"Just start typing...": "Just start typing...", "Just start typing...": "Just start typing...",
"Knowledge base": "Knowledge base", "Knowledge base": "Knowledge base",
"Last rev.": "Посл. изм.",
"Link sent, check your email": "Link sent, check your email", "Link sent, check your email": "Link sent, check your email",
"Lists": "Lists",
"Literature": "Literature", "Literature": "Literature",
"Load more": "Show more", "Load more": "Show more",
"Loading": "Loading", "Loading": "Loading",
"Logout": "Logout",
"Manifest": "Manifest", "Manifest": "Manifest",
"More": "More", "More": "More",
"Most commented": "Commented", "Most commented": "Commented",
@ -117,6 +121,7 @@
"My feed": "My feed", "My feed": "My feed",
"My subscriptions": "Subscriptions", "My subscriptions": "Subscriptions",
"Name": "Name", "Name": "Name",
"New only": "New only",
"New password": "New password", "New password": "New password",
"New stories every day and even more!": "New stories and more are waiting for you every day!", "New stories every day and even more!": "New stories and more are waiting for you every day!",
"No such account, please try to register": "No such account found, please try to register", "No such account, please try to register": "No such account found, please try to register",
@ -124,13 +129,14 @@
"Nothing is here": "There is nothing here", "Nothing is here": "There is nothing here",
"Or continue with social network": "Or continue with social network", "Or continue with social network": "Or continue with social network",
"Our regular contributor": "Our regular contributor", "Our regular contributor": "Our regular contributor",
"Paragraphs": "Абзацев",
"Participating": "Participating", "Participating": "Participating",
"Partners": "Partners", "Partners": "Partners",
"Password": "Password", "Password": "Password",
"Password again": "Password again", "Password again": "Password again",
"Passwords are not equal": "Passwords are not equal", "Passwords are not equal": "Passwords are not equal",
"Paste Embed code": "Paste Embed code",
"Personal": "Personal", "Personal": "Personal",
"personal data usage and email notifications": "to process personal data and receive email notifications",
"Pin": "Pin", "Pin": "Pin",
"Please check your email address": "Please check your email address", "Please check your email address": "Please check your email address",
"Please confirm your email to finish": "Confirm your email and the action will complete", "Please confirm your email to finish": "Confirm your email and the action will complete",
@ -141,19 +147,19 @@
"Please, confirm email": "Please confirm email", "Please, confirm email": "Please confirm email",
"Popular": "Popular", "Popular": "Popular",
"Popular authors": "Popular authors", "Popular authors": "Popular authors",
"post": "post",
"Principles": "Community principles", "Principles": "Community principles",
"Profile": "Profile", "Profile": "Profile",
"Profile settings": "Profile settings", "Profile settings": "Profile settings",
"Publications": "Publications", "Publications": "Publications",
"Quit": "Quit", "Quit": "Quit",
"Quotes": "Quotes",
"Reason uknown": "Reason unknown", "Reason uknown": "Reason unknown",
"Recent": "Fresh", "Recent": "Fresh",
"register": "register",
"Reply": "Reply", "Reply": "Reply",
"Report": "Complain", "Report": "Complain",
"Resend code": "Send confirmation", "Resend code": "Send confirmation",
"Restore password": "Restore password", "Restore password": "Restore password",
"Save draft": "Save draft",
"Save settings": "Save settings", "Save settings": "Save settings",
"Search": "Search", "Search": "Search",
"Search author": "Search author", "Search author": "Search author",
@ -165,10 +171,8 @@
"Send link again": "Send link again", "Send link again": "Send link again",
"Settings": "Settings", "Settings": "Settings",
"Share": "Share", "Share": "Share",
"shout": "post", "Short opening": "Short opening",
"Show": "Show", "Show": "Show",
"sign up or sign in": "sign up or sign in",
"slug is used by another user": "Slug is already taken by another user",
"Social networks": "Social networks", "Social networks": "Social networks",
"Something went wrong, check email and password": "Something went wrong. Check your email and password", "Something went wrong, check email and password": "Something went wrong. Check your email and password",
"Something went wrong, please try again": "Something went wrong, please try again", "Something went wrong, please try again": "Something went wrong, please try again",
@ -184,10 +188,11 @@
"Successfully authorized": "Authorization successful", "Successfully authorized": "Authorization successful",
"Suggest an idea": "Suggest an idea", "Suggest an idea": "Suggest an idea",
"Support us": "Help the magazine", "Support us": "Help the magazine",
"terms of use": "terms of use",
"Terms of use": "Site rules", "Terms of use": "Site rules",
"Thank you": "Thank you", "Thank you": "Thank you",
"This comment has not yet been rated": "This comment has not yet been rated",
"This email is already taken. If it's you": "This email is already taken. If it's you", "This email is already taken. If it's you": "This email is already taken. If it's you",
"This post has not been rated yet": "This post has not been rated yet",
"To leave a comment please": "To leave a comment please", "To leave a comment please": "To leave a comment please",
"To write a comment, you must": "To write a comment, you must", "To write a comment, you must": "To write a comment, you must",
"Top authors": "Authors rating", "Top authors": "Authors rating",
@ -199,17 +204,15 @@
"Top topics": "Interesting topics", "Top topics": "Interesting topics",
"Top viewed": "Most viewed", "Top viewed": "Most viewed",
"Topic is supported by": "Topic is supported by", "Topic is supported by": "Topic is supported by",
"topics": "topics", "Topic subscriptions": "Подписки на темы",
"Topics": "Topics", "Topics": "Topics",
"Topics which supported by author": "Topics which supported by author", "Topics which supported by author": "Topics which supported by author",
"Try to find another way": "Try to find another way", "Try to find another way": "Try to find another way",
"Unfollow": "Unfollow", "Unfollow": "Unfollow",
"Unfollow the topic": "Unfollow the topic", "Unfollow the topic": "Unfollow the topic",
"user already exist": "user already exists",
"Username": "Username", "Username": "Username",
"Userpic": "Userpic", "Userpic": "Userpic",
"Video": "Video", "Video": "Video",
"view": "view",
"Views": "Views", "Views": "Views",
"We are convinced that one voice is good, but many is better": "We are convinced that one voice is good, but many is better", "We are convinced that one voice is good, but many is better": "We are convinced that one voice is good, but many is better",
"We can't find you, check email or": "We can't find you, check email or", "We can't find you, check email or": "We can't find you, check email or",
@ -217,10 +220,12 @@
"We know you, please try to login": "This email address is already registered, please try to login", "We know you, please try to login": "This email address is already registered, please try to login",
"We've sent you a message with a link to enter our website.": "We've sent you an email with a link to your email. Follow the link in the email to enter our website.", "We've sent you a message with a link to enter our website.": "We've sent you an email with a link to your email. Follow the link in the email to enter our website.",
"Where": "From", "Where": "From",
"Words": "Слов",
"Work with us": "Cooperate with Discourse", "Work with us": "Cooperate with Discourse",
"Write": "Write", "Write": "Write",
"Write a comment...": "Write a comment...", "Write a comment...": "Write a comment...",
"Write about the topic": "Write about the topic", "Write about the topic": "Write about the topic",
"Write an article": "Write an article",
"Write comment": "Write comment", "Write comment": "Write comment",
"Write message": "Write a message", "Write message": "Write a message",
"Write to us": "Write to us", "Write to us": "Write to us",
@ -229,18 +234,35 @@
"You've confirmed email": "You've confirmed email", "You've confirmed email": "You've confirmed email",
"You've reached a non-existed page": "You've reached a non-existed page", "You've reached a non-existed page": "You've reached a non-existed page",
"Your name will appear on your profile page and as your signature in publications, comments and responses.": "Your name will appear on your profile page and as your signature in publications, comments and responses", "Your name will appear on your profile page and as your signature in publications, comments and responses.": "Your name will appear on your profile page and as your signature in publications, comments and responses",
"zine": "zine", "accomplices": "accomplices",
"By time": "By time", "actions": "actions",
"New only": "New only", "all topics": "all topics",
"Short opening": "Short opening", "author": "author",
"Write an article": "Write an article", "authors": "authors",
"Enter URL address": "Enter URL address", "bookmarks": "bookmarks",
"Invalid url format": "Invalid url format", "cancel": "Cancel",
"Headers": "Headers", "collections": "collections",
"Quotes": "Quotes", "community": "community",
"Lists": "Lists", "discussion": "discourse",
"Bookmarks": "Bookmarks", "discussions": "discussions",
"Logout": "Logout", "drafts": "drafts",
"This comment has not yet been rated": "This comment has not yet been rated", "email not confirmed": "email not confirmed",
"This post has not been rated yet": "This post has not been rated yet" "enter": "enter",
"feed": "feed",
"follower": "follower",
"general feed": "general tape",
"invalid password": "invalid password",
"my feed": "my ribbon",
"notifications": "notifications",
"personal data usage and email notifications": "to process personal data and receive email notifications",
"post": "post",
"register": "register",
"shout": "post",
"sign up or sign in": "sign up or sign in",
"slug is used by another user": "Slug is already taken by another user",
"terms of use": "terms of use",
"topics": "topics",
"user already exist": "user already exists",
"view": "view",
"zine": "zine"
} }

View File

@ -14,10 +14,12 @@
"Artworks": "Артворки", "Artworks": "Артворки",
"Audio": "Аудио", "Audio": "Аудио",
"Author": "Автор", "Author": "Автор",
"Author subscriptions": "Подписки на авторов",
"Authors": "Авторы", "Authors": "Авторы",
"Back to main page": "Вернуться на главную", "Back to main page": "Вернуться на главную",
"Become an author": "Стать автором", "Become an author": "Стать автором",
"Bookmarked": "Сохранено", "Bookmarked": "Сохранено",
"Bookmarks": "Закладки",
"By alphabet": "По алфавиту", "By alphabet": "По алфавиту",
"By authors": "По авторам", "By authors": "По авторам",
"By name": "По имени", "By name": "По имени",
@ -26,9 +28,11 @@
"By relevance": "По релевантности", "By relevance": "По релевантности",
"By shouts": "По публикациям", "By shouts": "По публикациям",
"By signing up you agree with our": "Регистрируясь, вы соглашаетесь с", "By signing up you agree with our": "Регистрируясь, вы соглашаетесь с",
"By time": "По порядку",
"By title": "По названию", "By title": "По названию",
"By updates": "По обновлениям", "By updates": "По обновлениям",
"By views": "По просмотрам", "By views": "По просмотрам",
"Characters": "Знаков",
"Chat Title": "Тема дискурса", "Chat Title": "Тема дискурса",
"Choose who you want to write to": "Выберите кому хотите написать", "Choose who you want to write to": "Выберите кому хотите написать",
"Collaborate": "Помочь редактировать", "Collaborate": "Помочь редактировать",
@ -56,6 +60,7 @@
"Editing": "Редактирование", "Editing": "Редактирование",
"Email": "Почта", "Email": "Почта",
"Enter": "Войти", "Enter": "Войти",
"Enter URL address": "Введите адрес ссылки",
"Enter text": "Введите текст", "Enter text": "Введите текст",
"Enter the Discours": "Войти в Дискурс", "Enter the Discours": "Войти в Дискурс",
"Enter the code or click the link from email to confirm": "Введите код из письма или пройдите по ссылке в письме для подтверждения регистрации", "Enter the code or click the link from email to confirm": "Введите код из письма или пройдите по ссылке в письме для подтверждения регистрации",
@ -80,12 +85,16 @@
"Group Chat": "Общий чат", "Group Chat": "Общий чат",
"Groups": "Группы", "Groups": "Группы",
"Header": "Заголовок", "Header": "Заголовок",
"Headers": "Заголовки",
"Help": "Помощь",
"Help to edit": "Помочь редактировать", "Help to edit": "Помочь редактировать",
"Here you can customize your profile the way you want.": "Здесь можно настроить свой профиль так, как вы хотите.", "Here you can customize your profile the way you want.": "Здесь можно настроить свой профиль так, как вы хотите.",
"Hooray! Welcome!": "Ура! Добро пожаловать!", "Hooray! Welcome!": "Ура! Добро пожаловать!",
"Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики", "Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики",
"Hotkeys": "Горячие клавиши",
"How can I help/skills": "Чем могу помочь/навыки", "How can I help/skills": "Чем могу помочь/навыки",
"How it works": "Как это работает", "How it works": "Как это работает",
"How to write a good article": "Как написать хорошую статью",
"How to write an article": "Как написать статью", "How to write an article": "Как написать статью",
"I have an account": "У меня есть аккаунт!", "I have an account": "У меня есть аккаунт!",
"I have no account yet": "У меня еще нет аккаунта", "I have no account yet": "У меня еще нет аккаунта",
@ -94,6 +103,7 @@
"Introduce": "Представление", "Introduce": "Представление",
"Invalid email": "Проверьте правильность ввода почты", "Invalid email": "Проверьте правильность ввода почты",
"Invite co-authors": "Пригласить соавторов", "Invite co-authors": "Пригласить соавторов",
"Invalid url format": "Неверный формат ссылки",
"Invite experts": "Пригласить экспертов", "Invite experts": "Пригласить экспертов",
"Invite to collab": "Пригласить к участию", "Invite to collab": "Пригласить к участию",
"It does not look like url": "Это не похоже на ссылку", "It does not look like url": "Это не похоже на ссылку",
@ -104,10 +114,13 @@
"Just start typing...": "Просто начните печатать...", "Just start typing...": "Просто начните печатать...",
"Karma": "Карма", "Karma": "Карма",
"Knowledge base": "База знаний", "Knowledge base": "База знаний",
"Last rev.": "Посл. изм.",
"Link sent, check your email": "Ссылка отправлена, проверьте почту", "Link sent, check your email": "Ссылка отправлена, проверьте почту",
"Lists": "Списки",
"Literature": "Литература", "Literature": "Литература",
"Load more": "Показать ещё", "Load more": "Показать ещё",
"Loading": "Загрузка", "Loading": "Загрузка",
"Logout": "Выход",
"Manifest": "Манифест", "Manifest": "Манифест",
"More": "Ещё", "More": "Ещё",
"Most commented": "Комментируемое", "Most commented": "Комментируемое",
@ -115,6 +128,7 @@
"My feed": "Моя лента", "My feed": "Моя лента",
"My subscriptions": "Подписки", "My subscriptions": "Подписки",
"Name": "Имя", "Name": "Имя",
"New only": "Только новые",
"New password": "Новый пароль", "New password": "Новый пароль",
"New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!", "New stories every day and even more!": "Каждый день вас ждут новые истории и ещё много всего интересного!",
"No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться", "No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться",
@ -122,11 +136,13 @@
"Nothing is here": "Здесь ничего нет", "Nothing is here": "Здесь ничего нет",
"Or continue with social network": "Или продолжите через соцсеть", "Or continue with social network": "Или продолжите через соцсеть",
"Our regular contributor": "Наш постоянный автор", "Our regular contributor": "Наш постоянный автор",
"Paragraphs": "Абзацев",
"Participating": "Участвовать", "Participating": "Участвовать",
"Partners": "Партнёры", "Partners": "Партнёры",
"Password": "Пароль", "Password": "Пароль",
"Password again": "Пароль ещё раз", "Password again": "Пароль ещё раз",
"Passwords are not equal": "Пароли не совпадают", "Passwords are not equal": "Пароли не совпадают",
"Paste Embed code": "Вставьте embed код",
"Personal": "Личные", "Personal": "Личные",
"Pin": "Закрепить", "Pin": "Закрепить",
"Please check your email address": "Пожалуйста, проверьте введенный адрес почты", "Please check your email address": "Пожалуйста, проверьте введенный адрес почты",
@ -147,6 +163,7 @@
"Publication settings": "Настройки публикации", "Publication settings": "Настройки публикации",
"Publish": "Опубликовать", "Publish": "Опубликовать",
"Quit": "Выйти", "Quit": "Выйти",
"Quotes": "Цитаты",
"Reason uknown": "Причина неизвестна", "Reason uknown": "Причина неизвестна",
"Recent": "Свежее", "Recent": "Свежее",
"Reply": "Ответить", "Reply": "Ответить",
@ -154,8 +171,8 @@
"Resend code": "Выслать подтверждение", "Resend code": "Выслать подтверждение",
"Restore password": "Восстановить пароль", "Restore password": "Восстановить пароль",
"Save": "Сохранить", "Save": "Сохранить",
"Save settings": "Сохранить настройки",
"Save draft": "Сохранить черновик", "Save draft": "Сохранить черновик",
"Save settings": "Сохранить настройки",
"Search": "Поиск", "Search": "Поиск",
"Search author": "Поиск автора", "Search author": "Поиск автора",
"Search topic": "Поиск темы", "Search topic": "Поиск темы",
@ -166,6 +183,7 @@
"Send link again": "Прислать ссылку ещё раз", "Send link again": "Прислать ссылку ещё раз",
"Settings": "Настройки", "Settings": "Настройки",
"Share": "Поделиться", "Share": "Поделиться",
"Short opening": "Небольшое вступление, чтобы заинтересовать читателя",
"Show": "Показать", "Show": "Показать",
"Social networks": "Социальные сети", "Social networks": "Социальные сети",
"Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль", "Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль",
@ -185,7 +203,9 @@
"Support us": "Помочь журналу", "Support us": "Помочь журналу",
"Terms of use": "Правила сайта", "Terms of use": "Правила сайта",
"Thank you": "Благодарности", "Thank you": "Благодарности",
"This comment has not yet been rated": "Этот комментарий еще пока никто не оценил",
"This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы", "This email is already taken. If it's you": "Такой email уже зарегистрирован. Если это вы",
"This post has not been rated yet": "Эту публикацию еще пока никто не оценил",
"To leave a comment please": "Чтобы оставить комментарий, необходимо", "To leave a comment please": "Чтобы оставить комментарий, необходимо",
"To write a comment, you must": "Чтобы написать комментарий, необходимо", "To write a comment, you must": "Чтобы написать комментарий, необходимо",
"Top authors": "Рейтинг авторов", "Top authors": "Рейтинг авторов",
@ -197,6 +217,7 @@
"Top topics": "Интересные темы", "Top topics": "Интересные темы",
"Top viewed": "Самое читаемое", "Top viewed": "Самое читаемое",
"Topic is supported by": "Тему поддерживают", "Topic is supported by": "Тему поддерживают",
"Topic subscriptions": "Подписки на темы",
"Topics": "Темы", "Topics": "Темы",
"Topics which supported by author": "Автор поддерживает темы", "Topics which supported by author": "Автор поддерживает темы",
"Try to find another way": "Попробуйте найти по-другому", "Try to find another way": "Попробуйте найти по-другому",
@ -213,10 +234,12 @@
"We've sent you a message with a link to enter our website.": "Мы выслали вам письмо с ссылкой на почту. Перейдите по ссылке в письме, чтобы войти на сайт.", "We've sent you a message with a link to enter our website.": "Мы выслали вам письмо с ссылкой на почту. Перейдите по ссылке в письме, чтобы войти на сайт.",
"Welcome!": "Добро пожаловать!", "Welcome!": "Добро пожаловать!",
"Where": "Откуда", "Where": "Откуда",
"Words": "Слов",
"Work with us": "Сотрудничать с Дискурсом", "Work with us": "Сотрудничать с Дискурсом",
"Write": "Написать", "Write": "Написать",
"Write a comment...": "Написать комментарий...", "Write a comment...": "Написать комментарий...",
"Write about the topic": "Написать в тему", "Write about the topic": "Написать в тему",
"Write an article": "Написать статью",
"Write comment": "Написать комментарий", "Write comment": "Написать комментарий",
"Write message": "Написать сообщение", "Write message": "Написать сообщение",
"Write to us": "Напишите нам", "Write to us": "Напишите нам",
@ -226,10 +249,12 @@
"You've reached a non-existed page": "Вы попали на несуществующую страницу", "You've reached a non-existed page": "Вы попали на несуществующую страницу",
"You've successfully logged out": "Вы успешно вышли из аккаунта", "You've successfully logged out": "Вы успешно вышли из аккаунта",
"Your name will appear on your profile page and as your signature in publications, comments and responses.": "Ваше имя появится на странице вашего профиля и как ваша подпись в публикациях, комментариях и откликах", "Your name will appear on your profile page and as your signature in publications, comments and responses.": "Ваше имя появится на странице вашего профиля и как ваша подпись в публикациях, комментариях и откликах",
"accomplices": "соучастники",
"actions": "действия", "actions": "действия",
"all topics": "все темы", "all topics": "все темы",
"author": "автор", "author": "автор",
"authors": "авторы", "authors": "авторы",
"bookmarks": "закладки",
"cancel": "Отмена", "cancel": "Отмена",
"collections": "коллекции", "collections": "коллекции",
"community": "сообщество", "community": "сообщество",
@ -237,11 +262,16 @@
"create_group": "Создать группу", "create_group": "Создать группу",
"discourse_theme": "Тема дискурса", "discourse_theme": "Тема дискурса",
"discussion": "дискурс", "discussion": "дискурс",
"discussions": "дискуссии",
"drafts": "черновики",
"email not confirmed": "email не подтвержден", "email not confirmed": "email не подтвержден",
"enter": "войдите", "enter": "войдите",
"feed": "лента", "feed": "лента",
"follower": "подписчик", "follower": "подписчик",
"general feed": "общая лента",
"invalid password": "некорректный пароль", "invalid password": "некорректный пароль",
"my feed": "моя лента",
"notifications": "уведомления",
"or": "или", "or": "или",
"personal data usage and email notifications": "на обработку персональных данных и на получение почтовых уведомлений", "personal data usage and email notifications": "на обработку персональных данных и на получение почтовых уведомлений",
"post": "пост", "post": "пост",
@ -255,18 +285,5 @@
"topics": "темы", "topics": "темы",
"user already exist": "пользователь уже существует", "user already exist": "пользователь уже существует",
"view": "просмотр", "view": "просмотр",
"zine": "журнал", "zine": "журнал"
"By time": "По порядку",
"New only": "Только новые",
"Short opening": "Небольшое вступление, чтобы заинтересовать читателя",
"Write an article": "Написать статью",
"Enter URL address": "Введите адрес ссылки",
"Invalid url format": "Неверный формат ссылки",
"Headers": "Заголовки",
"Quotes": "Цитаты",
"Lists": "Списки",
"Bookmarks": "Закладки",
"Logout": "Выход",
"This comment has not yet been rated": "Этот комментарий еще пока никто не оценил",
"This post has not been rated yet": "Эту публикацию еще пока никто не оценил"
} }

View File

@ -37,6 +37,7 @@ import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page' import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
import { SnackbarProvider } from '../context/snackbar' import { SnackbarProvider } from '../context/snackbar'
import { LocalizeProvider } from '../context/localize' import { LocalizeProvider } from '../context/localize'
import { EditorProvider } from '../context/editor'
// TODO: lazy load // TODO: lazy load
// const SomePage = lazy(() => import('./Pages/SomePage')) // const SomePage = lazy(() => import('./Pages/SomePage'))
@ -93,11 +94,13 @@ export const App = (props: PageProps) => {
return ( return (
<LocalizeProvider> <LocalizeProvider>
<EditorProvider>
<SnackbarProvider> <SnackbarProvider>
<SessionProvider> <SessionProvider>
<Dynamic component={pageComponent()} {...props} /> <Dynamic component={pageComponent()} {...props} />
</SessionProvider> </SessionProvider>
</SnackbarProvider> </SnackbarProvider>
</EditorProvider>
</LocalizeProvider> </LocalizeProvider>
) )
} }

View File

@ -186,9 +186,27 @@ img {
} }
} }
} }
}
.shoutStatsItemInner {
cursor: pointer;
margin: -0.3em -0.3em 0;
padding: 0.3em;
.icon {
margin-right: 0;
}
.iconEdit {
margin-right: 0.3em;
}
&:hover { &:hover {
background: #000;
cursor: pointer; cursor: pointer;
img {
filter: invert(1);
}
} }
} }

View File

@ -34,6 +34,7 @@ interface MediaItem {
const MediaView = (props: { media: MediaItem; kind: Shout['layout'] }) => { const MediaView = (props: { media: MediaItem; kind: Shout['layout'] }) => {
const { t } = useLocalize() const { t } = useLocalize()
return ( return (
<> <>
<Switch fallback={<a href={props.media.url}>{t('Cannot show this media type')}</a>}> <Switch fallback={<a href={props.media.url}>{t('Cannot show this media type')}</a>}>
@ -84,12 +85,11 @@ export const FullArticle = (props: ArticleProps) => {
const body = createMemo(() => props.article.body) const body = createMemo(() => props.article.body)
const media = createMemo(() => { const media = createMemo(() => {
const mi = JSON.parse(props.article.media || '[]') const mi = JSON.parse(props.article.media || '[]')
console.debug(mi) console.debug('!!! media items', mi)
return mi return mi
}) })
const commentsRef: { current: HTMLDivElement } = { current: null } const commentsRef: { current: HTMLDivElement } = { current: null }
const scrollToComments = () => { const scrollToComments = () => {
window.scrollTo({ window.scrollTo({
top: commentsRef.current.offsetTop - 96, top: commentsRef.current.offsetTop - 96,
@ -97,12 +97,20 @@ export const FullArticle = (props: ArticleProps) => {
behavior: 'smooth' behavior: 'smooth'
}) })
} }
const { searchParams } = useRouter()
createEffect(() => { createEffect(() => {
if (props.scrollToComments) { if (props.scrollToComments) {
scrollToComments() scrollToComments()
} }
}) })
const { changeSearchParam } = useRouter()
createEffect(() => {
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) {
scrollToComments()
changeSearchParam('scrollTo', null)
}
})
const { const {
actions: { loadReactionsBy } actions: { loadReactionsBy }

View File

@ -1,3 +1,4 @@
import { createEffect } from 'solid-js'
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap' import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Blockquote } from '@tiptap/extension-blockquote' import { Blockquote } from '@tiptap/extension-blockquote'
@ -19,7 +20,6 @@ import { HardBreak } from '@tiptap/extension-hard-break'
import { Heading } from '@tiptap/extension-heading' import { Heading } from '@tiptap/extension-heading'
import { Highlight } from '@tiptap/extension-highlight' import { Highlight } from '@tiptap/extension-highlight'
import { Link } from '@tiptap/extension-link' import { Link } from '@tiptap/extension-link'
import { Youtube } from '@tiptap/extension-youtube'
import { Document } from '@tiptap/extension-document' import { Document } from '@tiptap/extension-document'
import { Text } from '@tiptap/extension-text' import { Text } from '@tiptap/extension-text'
import { Image } from '@tiptap/extension-image' import { Image } from '@tiptap/extension-image'
@ -37,6 +37,8 @@ import { IndexeddbPersistence } from 'y-indexeddb'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import uniqolor from 'uniqolor' import uniqolor from 'uniqolor'
import { HocuspocusProvider } from '@hocuspocus/provider' import { HocuspocusProvider } from '@hocuspocus/provider'
import { Embed } from './extensions/embed'
import { useEditorContext } from '../../context/editor'
type EditorProps = { type EditorProps = {
shoutSlug: string shoutSlug: string
@ -128,7 +130,7 @@ export const Editor = (props: EditorProps) => {
HardBreak, HardBreak,
Highlight, Highlight,
Image, Image,
Youtube, Embed,
TrailingNode, TrailingNode,
BubbleMenu.configure({ BubbleMenu.configure({
element: bubbleMenuRef.current element: bubbleMenuRef.current
@ -142,6 +144,22 @@ export const Editor = (props: EditorProps) => {
] ]
})) }))
const html = useEditorHTML(() => editor())
const {
actions: { countWords }
} = useEditorContext()
createEffect(() => {
props.onChange(html())
if (html()) {
countWords({
characters: editor().storage.characterCount.characters(),
words: editor().storage.characterCount.words()
})
}
})
return ( return (
<> <>
<div ref={(el) => (editorElRef.current = el)} /> <div ref={(el) => (editorElRef.current = el)} />

View File

@ -5,7 +5,8 @@ import { Icon } from '../../_shared/Icon'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEditorTransaction } from 'solid-tiptap' import { createEditorTransaction } from 'solid-tiptap'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { LinkForm } from './LinkForm' import { InlineForm } from '../InlineForm'
import validateUrl from '../../../utils/validateUrl'
type BubbleMenuProps = { type BubbleMenuProps = {
editor: Editor editor: Editor
@ -55,12 +56,37 @@ export const EditorBubbleMenu = (props: BubbleMenuProps) => {
setListBubbleOpen((prev) => !prev) setListBubbleOpen((prev) => !prev)
} }
const handleLinkFormSubmit = (value: string) => {
props.editor.chain().focus().setLink({ href: value }).run()
}
const currentUrl = createEditorTransaction(
() => props.editor,
(editor) => {
return (editor && editor.getAttributes('link').href) || ''
}
)
const handleClearLinkForm = () => {
if (currentUrl()) {
props.editor.chain().focus().unsetLink().run()
}
setLinkEditorOpen(false)
}
return ( return (
<> <>
<div ref={props.ref} class={styles.bubbleMenu}> <div ref={props.ref} class={styles.bubbleMenu}>
<Switch> <Switch>
<Match when={linkEditorOpen()}> <Match when={linkEditorOpen()}>
<LinkForm editor={props.editor} onClose={() => setLinkEditorOpen(false)} /> <InlineForm
variant="inBubble"
initialValue={currentUrl() ?? ''}
onClear={handleClearLinkForm}
validate={(value) => (validateUrl(value) ? '' : t('Invalid url format'))}
onSubmit={handleLinkFormSubmit}
onClose={() => setLinkEditorOpen(false)}
/>
</Match> </Match>
<Match when={!linkEditorOpen()}> <Match when={!linkEditorOpen()}>
<> <>

View File

@ -6,5 +6,10 @@
button { button {
opacity: 0.3; opacity: 0.3;
vertical-align: text-bottom; vertical-align: text-bottom;
transition: opacity 0.3s ease-in-out;
&:hover {
opacity: 1;
}
} }
} }

View File

@ -1,19 +1,51 @@
import { createSignal, Show } from 'solid-js'
import type { Editor } from '@tiptap/core' import type { Editor } from '@tiptap/core'
import styles from './EditorFloatingMenu.module.scss'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { createSignal } from 'solid-js' import { InlineForm } from './InlineForm'
import styles from './EditorFloatingMenu.module.scss'
import HTMLParser from 'html-to-json-parser'
type FloatingMenuProps = { type FloatingMenuProps = {
editor: Editor editor: Editor
ref: (el: HTMLDivElement) => void ref: (el: HTMLDivElement) => void
} }
const embedData = async (data) => {
const result = await HTMLParser(data, false)
if (result && 'type' in result && result.type === 'iframe') {
return result.attributes
}
}
export const EditorFloatingMenu = (props: FloatingMenuProps) => { export const EditorFloatingMenu = (props: FloatingMenuProps) => {
const [inlineEditorOpen, setInlineEditorOpen] = createSignal<boolean>(false)
const handleEmbedFormSubmit = async (value: string) => {
// TODO: add support instagram embed (blockquote)
const emb = await embedData(value)
props.editor.chain().focus().setIframe(emb).run()
}
const validateEmbed = async (value) => {
const iframeData = await HTMLParser(value, false)
if (iframeData && iframeData.type !== 'iframe') {
return
}
}
return ( return (
<div ref={props.ref} class={styles.editorFloatingMenu}> <div ref={props.ref} class={styles.editorFloatingMenu}>
<button> <button type="button" onClick={() => setInlineEditorOpen(true)}>
<Icon name="editor-plus" /> <Icon name="editor-plus" />
</button> </button>
<Show when={inlineEditorOpen()}>
<InlineForm
variant="inFloating"
onClose={() => setInlineEditorOpen(false)}
validate={validateEmbed}
onSubmit={handleEmbedFormSubmit}
/>
</Show>
</div> </div>
) )
} }

View File

@ -0,0 +1,67 @@
.InlineForm {
position: relative;
&.inBubble {
//...
}
&.inFloating {
position: absolute;
left: calc(100% + 1rem);
top: -0.8rem;
min-width: 64vw;
background: #fff;
box-shadow: 0 4px 10px rgba(#000, 0.25);
button {
opacity: 1;
&:disabled,
&:disabled:hover {
opacity: 0.3;
}
}
}
.form {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
padding: 6px 11px;
input {
margin: 0 12px 0 0;
padding: 0;
flex: 1;
border: none;
min-width: 200px;
display: block;
&::placeholder {
color: rgba(#000, 0.3);
}
&:focus {
outline: none;
}
}
}
.linkError {
padding: 6px 11px;
color: red;
font-size: 0.7em;
position: absolute;
bottom: -3rem;
left: 0;
right: 0;
height: 0;
background: #fff;
box-shadow: 0 4px 10px rgba(#000, 0.25);
opacity: 0;
transition: height 0.3s ease-in-out, opacity 0.3s ease-in-out;
&.visible {
height: 32px;
opacity: 1;
}
}
}

View File

@ -0,0 +1,90 @@
import styles from './InlineForm.module.scss'
import { Icon } from '../../_shared/Icon'
import { createEditorTransaction } from 'solid-tiptap'
import type { Editor } from '@tiptap/core'
import { createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { clsx } from 'clsx'
type Props = {
onClose: () => void
onClear?: () => void
onSubmit: (value: string) => void
variant: 'inBubble' | 'inFloating'
validate?: (value: string) => string | Promise<string>
initialValue?: string
}
export const InlineForm = (props: Props) => {
const { t } = useLocalize()
const [formValue, setFormValue] = createSignal(props.initialValue || '')
const [formValueError, setFormValueError] = createSignal('')
const handleFormInput = (value) => {
setFormValue(value)
}
const handleSaveButtonClick = async () => {
const errorMessage = await props.validate(formValue())
if (errorMessage) {
setFormValueError(errorMessage)
return
}
props.onSubmit(formValue())
props.onClose()
}
const handleKeyPress = async (event) => {
setFormValueError('')
const key = event.key
if (key === 'Enter') {
await handleSaveButtonClick()
}
if (key === 'Esc') {
props.onClear
}
}
return (
<div
class={clsx(styles.InlineForm, {
[styles.inBubble]: props.variant === 'inBubble',
[styles.inFloating]: props.variant === 'inFloating'
})}
>
<div class={styles.form}>
<Show when={props.variant === 'inBubble'}>
<input
type="text"
placeholder={t('Enter URL address')}
autofocus
value={props.initialValue}
onKeyPress={(e) => handleKeyPress(e)}
onInput={(e) => handleFormInput(e.currentTarget.value)}
/>
</Show>
<Show when={props.variant === 'inFloating'}>
<input
autofocus
type="text"
placeholder={t('Paste Embed code')}
onKeyPress={(e) => handleKeyPress(e)}
onInput={(e) => handleFormInput(e.currentTarget.value)}
/>
</Show>
<button type="button" onClick={handleSaveButtonClick} disabled={formValueError() !== ''}>
<Icon name="status-done" />
</button>
<button type="button" onClick={props.onClear}>
{props.initialValue ? <Icon name="editor-unlink" /> : <Icon name="status-cancel" />}
</button>
</div>
<div class={clsx(styles.linkError, { [styles.visible]: Boolean(formValueError()) })}>
{formValueError()}
</div>
</div>
)
}

View File

@ -0,0 +1 @@
export { InlineForm } from './InlineForm'

View File

@ -0,0 +1,93 @@
.Panel {
background: #1f1f1f;
color: rgb(255 255 255 / 0.35);
display: flex;
flex-direction: column;
font-size: 1.7rem;
justify-content: flex-start;
height: 100%;
line-height: 1.4;
padding: $grid-gutter-width $grid-gutter-width / 2;
position: fixed;
transition: transform 0.3s;
right: 0;
top: 0;
z-index: 10;
.close {
filter: invert(1);
margin: -1.6rem 0 0 -1.6rem;
}
.actionsHolder {
padding: 0 $grid-gutter-width / 2;
&.scrolled {
overflow-y: auto;
scroll-behavior: smooth;
}
}
section {
border-bottom: 2px solid rgb(255 255 255 / 0.1);
padding: 1.8rem 0;
&:first-child {
padding-top: 0;
}
p {
margin: 0.6em 0;
&:last-child {
margin-bottom: 0;
}
}
}
.button {
font-weight: normal;
margin-left: -1.6rem;
text-align: left;
&:hover {
color: #fff;
text-decoration: none;
}
}
.buttonWithIcon {
margin-left: -1.6rem;
.icon {
filter: invert(0.5);
margin-right: 0.3em;
width: 1em;
}
img {
vertical-align: middle;
}
}
.stats {
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-end;
margin-top: 3em;
}
a {
color: rgb(255 255 255 / 0.35);
font-weight: normal !important;
&:hover {
background: none;
color: #fff;
}
}
&.hidden {
transform: translateX(100%);
}
}

View File

@ -0,0 +1,107 @@
import { Show } from 'solid-js'
import { clsx } from 'clsx'
import { Button } from '../../_shared/Button'
import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize'
import styles from './Panel.module.scss'
import { useEditorContext } from '../../../context/editor'
type Props = {
// isVisible: boolean
}
export const Panel = (props: Props) => {
const { t } = useLocalize()
const {
isEditorPanelVisible,
wordCounter,
actions: { toggleEditorPanel }
} = useEditorContext()
return (
<aside class={clsx('col-md-6', styles.Panel, { [styles.hidden]: !isEditorPanelVisible() })}>
<div class={styles.actionsHolder}>
<Button
value={<Icon name="close" />}
variant={'inline'}
class={styles.close}
onClick={() => toggleEditorPanel()}
/>
</div>
<div class={clsx(styles.actionsHolder, styles.scrolled)}>
<section>
<Button value={t('Publish')} variant={'inline'} class={styles.button} />
<Button value={t('Save draft')} variant={'inline'} class={styles.button} />
</section>
<section>
<Button
value={
<>
<Icon name="eye" class={styles.icon} />
{t('Preview')}
</>
}
variant={'inline'}
class={clsx(styles.button, styles.buttonWithIcon)}
/>
<Button
value={
<>
<Icon name="pencil-outline" class={styles.icon} />
{t('Editing')}
</>
}
variant={'inline'}
class={clsx(styles.button, styles.buttonWithIcon)}
/>
<Button
value={
<>
<Icon name="feed-discussion" class={styles.icon} />
{t('FAQ')}
</>
}
variant={'inline'}
class={clsx(styles.button, styles.buttonWithIcon)}
/>
</section>
<section>
<Button value={t('Invite co-authors')} variant={'inline'} class={styles.button} />
<Button value={t('Publication settings')} variant={'inline'} class={styles.button} />
<Button value={t('Corrections history')} variant={'inline'} class={styles.button} />
</section>
<section>
<p>
<a href="/how-to-write-a-good-article">{t('How to write a good article')}</a>
</p>
<p>
<a href="#">{t('Hotkeys')}</a>
</p>
<p>
<a href="#">{t('Help')}</a>
</p>
</section>
<div class={styles.stats}>
<div>
{t('Characters')}: <em>{wordCounter().characters}</em>
</div>
<div>
{t('Words')}: <em>{wordCounter().words}</em>
</div>
<Show when={wordCounter().paragraphs}>
<div>
{t('Paragraphs')}: <em>{wordCounter().paragraphs}</em>
</div>
</Show>
<div>
{t('Last rev.')}: <em>22.03.22 в 18:20</em>
</div>
</div>
</div>
</aside>
)
}

View File

@ -0,0 +1 @@
export { Panel } from './Panel'

View File

@ -47,3 +47,17 @@
user-select: none; user-select: none;
white-space: nowrap; white-space: nowrap;
} }
.embed-wrapper {
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
background: #f1f1f1;
margin: 4rem 0;
iframe {
border: none;
overflow: hidden;
}
}

View File

@ -0,0 +1,73 @@
import { mergeAttributes, Node } from '@tiptap/core'
import { NodeRange } from 'prosemirror-model'
import { insert } from 'solid-js/web'
import { TextSelection } from 'prosemirror-state'
export interface IframeOptions {
allowFullscreen: boolean
HTMLAttributes: {
[key: string]: any
}
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
iframe: {
setIframe: (options: { src: string }) => ReturnType
}
}
}
export const Embed = Node.create<IframeOptions>({
name: 'embed',
group: 'block',
selectable: true,
atom: true,
draggable: true,
addAttributes() {
return {
src: { default: null },
width: { default: null },
height: { default: null }
}
},
parseHTML() {
return [
{
tag: 'iframe'
}
]
},
renderHTML({ HTMLAttributes }) {
return ['iframe', mergeAttributes(HTMLAttributes)]
},
addNodeView() {
return ({ node }) => {
const div = document.createElement('div')
div.className = 'embed-wrapper'
const iframe = document.createElement('iframe')
iframe.width = node.attrs.width
iframe.height = node.attrs.height
iframe.allowfullscreen = node.attrs.allowfullscreen
iframe.src = node.attrs.src
div.append(iframe)
return {
dom: div
}
}
},
addCommands() {
return {
setIframe:
(options) =>
({ tr, dispatch }) => {
const { selection } = tr
const node = this.type.create(options)
if (dispatch) {
tr.replaceRangeWith(selection.from, selection.to, node)
}
return true
}
}
}
})

View File

@ -12,6 +12,8 @@ import stylesHeader from '../Nav/Header.module.scss'
import { getDescription } from '../../utils/meta' import { getDescription } from '../../utils/meta'
import { FeedArticlePopup } from './FeedArticlePopup' import { FeedArticlePopup } from './FeedArticlePopup'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { openPage } from '@nanostores/router'
import { router, useRouter } from '../../stores/router'
interface ArticleCardProps { interface ArticleCardProps {
settings?: { settings?: {
@ -35,6 +37,7 @@ interface ArticleCardProps {
isBeside?: boolean isBeside?: boolean
} }
article: Shout article: Shout
scrollTo: 'comments'
} }
const getTitleAndSubtitle = (article: Shout): { title: string; subtitle: string } => { const getTitleAndSubtitle = (article: Shout): { title: string; subtitle: string } => {
@ -75,6 +78,13 @@ export const ArticleCard = (props: ArticleCardProps) => {
const { cover, layout, slug, authors, stat, body } = props.article const { cover, layout, slug, authors, stat, body } = props.article
const { changeSearchParam } = useRouter()
const scrollToComments = (event) => {
event.preventDefault()
openPage(router, 'article', { slug: slug })
changeSearchParam('scrollTo', 'comments')
}
return ( return (
<section <section
class={clsx(styles.shoutCard, `${props.settings?.additionalClass || ''}`)} class={clsx(styles.shoutCard, `${props.settings?.additionalClass || ''}`)}
@ -172,7 +182,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
</div> </div>
<div class={clsx(styles.shoutCardDetailsItem, styles.shoutCardComments)}> <div class={clsx(styles.shoutCardDetailsItem, styles.shoutCardComments)}>
<a href={`/${slug + '#comments'}`}> <a href="#" onClick={(event) => scrollToComments(event)}>
<Icon name="comment" class={clsx(styles.icon, styles.feedControlIcon)} /> <Icon name="comment" class={clsx(styles.icon, styles.feedControlIcon)} />
{stat?.commented || t('Add comment')} {stat?.commented || t('Add comment')}
</a> </a>

View File

@ -1,83 +0,0 @@
.sidebar {
max-height: calc(100vh - 120px);
overflow: auto;
position: sticky;
top: 120px;
}
.sidebarItemName {
margin-right: 0.5em;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
}
.counter {
@include font-size(1.2rem);
align-items: center;
align-self: flex-start;
background: #f6f6f6;
border-radius: 0.8rem;
display: flex;
font-weight: bold;
justify-content: center;
min-width: 2em;
margin-left: 0.5em;
padding: 0.25em 0.5em 0.15em;
transition: background-color 0.2s;
}
.unread {
position: relative;
&::after {
background: #2638d9;
border-radius: 100%;
content: '';
display: inline-block;
height: 0.5em;
left: 100%;
margin-left: 0.3em;
position: absolute;
top: 0.5em;
width: 0.5em;
}
}
.settings {
display: flex;
justify-content: space-between;
margin-bottom: 2em;
}
a {
img {
transition: filter 0.3s;
}
&:hover {
img {
filter: invert(1);
}
.counter {
background: #000;
}
}
}
.icon {
display: inline-block;
line-height: 1;
height: 2rem;
margin-right: 0.5em;
vertical-align: middle;
width: 2.2rem;
img {
height: 100%;
object-fit: contain;
object-position: center;
width: 100%;
}
}

View File

@ -1,138 +0,0 @@
import { For } from 'solid-js'
import type { Author } from '../../graphql/types.gen'
import { useAuthorsStore } from '../../stores/zine/authors'
import { Icon } from '../_shared/Icon'
import { useTopicsStore } from '../../stores/zine/topics'
import { useArticlesStore } from '../../stores/zine/articles'
import { useSeenStore } from '../../stores/zine/seen'
import { useSession } from '../../context/session'
import styles from './Sidebar.module.scss'
import { useLocalize } from '../../context/localize'
type FeedSidebarProps = {
authors: Author[]
}
export const FeedSidebar = (props: FeedSidebarProps) => {
const { t } = useLocalize()
const { seen } = useSeenStore()
const { session } = useSession()
const { authorEntities } = useAuthorsStore({ authors: props.authors })
const { articlesByTopic } = useArticlesStore()
const { topicEntities } = useTopicsStore()
const checkTopicIsSeen = (topicSlug: string) => {
return articlesByTopic()[topicSlug]?.every((article) => Boolean(seen()[article.slug]))
}
const checkAuthorIsSeen = (authorSlug: string) => {
return Boolean(seen()[authorSlug])
}
return (
<div class={styles.sidebar}>
<ul>
<li>
<a href="#">
<span class={styles.sidebarItemName}>
<Icon name="feed-all" class={styles.icon} />
<strong>общая лента</strong>
</span>
</a>
</li>
<li>
<a href="#">
<span class={styles.sidebarItemName}>
<Icon name="feed-my" class={styles.icon} />
моя лента
</span>
</a>
</li>
<li>
<a href="#">
<span class={styles.sidebarItemName}>
<Icon name="feed-collaborate" class={styles.icon} />
соучастие
</span>
<span class={styles.counter}>7</span>
</a>
</li>
<li>
<a href="#">
<span class={styles.sidebarItemName}>
<Icon name="feed-discussion" class={styles.icon} />
дискуссии
</span>
<span class={styles.counter}>18</span>
</a>
</li>
<li>
<a href="#">
<span class={styles.sidebarItemName}>
<Icon name="feed-drafts" class={styles.icon} />
черновики
</span>
<span class={styles.counter}>283</span>
</a>
</li>
<li>
<a href="#">
<span class={styles.sidebarItemName}>
<Icon name="bookmark" class={styles.icon} />
закладки
</span>
</a>
</li>
<li>
<a href="#">
<span class={styles.sidebarItemName}>
<Icon name="feed-notifications" class={styles.icon} />
уведомления
</span>
<span class={styles.counter}>283</span>
</a>
</li>
</ul>
<ul>
<li>
<a href="/feed?by=subscribed">
<strong>{t('My subscriptions')}</strong>
</a>
</li>
<For each={session()?.news?.authors}>
{(authorSlug: string) => (
<li>
<a
href={`/author/${authorSlug}`}
classList={{ [styles.unread]: checkAuthorIsSeen(authorSlug) }}
>
<small>@{authorSlug}</small>
{authorEntities()[authorSlug]?.name}
</a>
</li>
)}
</For>
<For each={session()?.news?.topics}>
{(topicSlug: string) => (
<li>
<a href={`/author/${topicSlug}`} classList={{ [styles.unread]: checkTopicIsSeen(topicSlug) }}>
{topicEntities()[topicSlug]?.title}
</a>
</li>
)}
</For>
</ul>
<div class={styles.settings}>
<a href="/feed/settings">
<Icon name="settings" class={styles.icon} />
{t('Feed settings')}
</a>
</div>
</div>
)
}

View File

@ -0,0 +1,89 @@
.sidebar {
max-height: calc(100vh - 120px);
overflow: auto;
position: sticky;
top: 120px;
ul > li {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebarItemName {
margin-right: 0.5em;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
}
.counter {
@include font-size(1.2rem);
align-items: center;
align-self: flex-start;
background: #f6f6f6;
border-radius: 0.8rem;
display: inline-flex;
font-weight: bold;
justify-content: center;
min-width: 2em;
margin-left: 0.5em;
padding: 0.25em 0.5em 0.15em;
transition: background-color 0.2s;
}
.unread {
position: relative;
&::after {
background: #2638d9;
border-radius: 100%;
content: '';
display: inline-block;
height: 0.5em;
left: 100%;
margin-left: 0.3em;
position: absolute;
top: 0.5em;
width: 0.5em;
}
}
.settings {
display: flex;
justify-content: space-between;
margin-bottom: 2em;
}
a {
img {
transition: filter 0.3s;
}
&:hover {
img {
filter: invert(1);
}
.counter {
background: #000;
}
}
}
.icon {
display: inline-block;
line-height: 1;
height: 2rem;
margin-right: 0.5em;
vertical-align: middle;
width: 2.2rem;
img {
height: 100%;
object-fit: contain;
object-position: center;
width: 100%;
}
}
}

View File

@ -0,0 +1,130 @@
import { createSignal, For } from 'solid-js'
import type { Author } from '../../../graphql/types.gen'
import { useAuthorsStore } from '../../../stores/zine/authors'
import { Icon } from '../../_shared/Icon'
import { useTopicsStore } from '../../../stores/zine/topics'
import { useArticlesStore } from '../../../stores/zine/articles'
import { useSeenStore } from '../../../stores/zine/seen'
import { useSession } from '../../../context/session'
import { useLocalize } from '../../../context/localize'
import styles from './Sidebar.module.scss'
type FeedSidebarProps = {
authors: Author[]
}
type ListItem = {
title: string
icon?: string
counter?: number
href?: string
isBold?: boolean
}
export const Sidebar = (props: FeedSidebarProps) => {
const { t } = useLocalize()
const { seen } = useSeenStore()
const { session } = useSession()
const { authorEntities } = useAuthorsStore({ authors: props.authors })
const { articlesByTopic } = useArticlesStore()
const { topicEntities } = useTopicsStore()
createSignal(() => {
console.log('!!! topicEntities:', topicEntities())
})
const checkTopicIsSeen = (topicSlug: string) => {
return articlesByTopic()[topicSlug]?.every((article) => Boolean(seen()[article.slug]))
}
const checkAuthorIsSeen = (authorSlug: string) => {
return Boolean(seen()[authorSlug])
}
const menuItems: ListItem[] = [
{
icon: 'feed-all',
title: t('general feed')
},
{
icon: 'feed-my',
title: t('my feed')
},
{
icon: 'feed-collaborate',
title: t('accomplices')
},
{
icon: 'feed-discussion',
title: t('discussions'),
counter: 4
},
{
icon: 'feed-drafts',
title: t('drafts'),
counter: 14
},
{
icon: 'bookmark',
title: t('bookmarks'),
counter: 6
},
{
icon: 'feed-notifications',
title: t('notifications')
},
{
href: '/feed?by=subscribed',
title: t('My subscriptions'),
isBold: true
}
]
return (
<div class={styles.sidebar}>
<ul>
<For each={menuItems}>
{(item: ListItem, index) => (
<li key={index}>
<a href="#">
<span class={styles.sidebarItemName}>
{item.icon && <Icon name={item.icon} class={styles.icon} />}
<strong>{item.title}</strong>
{item.counter && <span class={styles.counter}>18</span>}
</span>
</a>
</li>
)}
</For>
<For each={session()?.news?.authors}>
{(authorSlug: string) => (
<li>
<a
href={`/author/${authorSlug}`}
classList={{ [styles.unread]: checkAuthorIsSeen(authorSlug) }}
>
<small>@{authorSlug}</small>
{authorEntities()[authorSlug]?.name}
</a>
</li>
)}
</For>
<For each={session()?.news?.topics}>
{(topicSlug: string) => (
<li>
<a href={`/author/${topicSlug}`} classList={{ [styles.unread]: checkTopicIsSeen(topicSlug) }}>
{topicEntities()[topicSlug]?.title ?? topicSlug}
</a>
</li>
)}
</For>
</ul>
<div class={styles.settings}>
<a href="/feed/settings">
<Icon name="settings" class={styles.icon} />
{t('Feed settings')}
</a>
</div>
</div>
)
}

View File

@ -0,0 +1 @@
export { Sidebar } from './Sidebar'

View File

@ -80,7 +80,8 @@ export const Header = (props: Props) => {
}) })
}) })
const scrollToComments = (value) => { const scrollToComments = (event, value) => {
event.preventDefault()
props.scrollToComments(value) props.scrollToComments(value)
} }
@ -110,7 +111,6 @@ export const Header = (props: Props) => {
<Show when={props.title}> <Show when={props.title}>
<div class={styles.articleHeader}>{props.title}</div> <div class={styles.articleHeader}>{props.title}</div>
</Show> </Show>
<ul <ul
class={clsx(styles.mainNavigation, 'col text-xl inline-flex')} class={clsx(styles.mainNavigation, 'col text-xl inline-flex')}
classList={{ fixed: fixed() }} classList={{ fixed: fixed() }}
@ -144,7 +144,7 @@ export const Header = (props: Props) => {
containerCssClass={styles.control} containerCssClass={styles.control}
trigger={<Icon name="share-outline" class={styles.icon} />} trigger={<Icon name="share-outline" class={styles.icon} />}
/> />
<div onClick={() => scrollToComments(true)} class={styles.control}> <div onClick={(event) => scrollToComments(event, true)} class={styles.control}>
<Icon name="comments-outline" class={styles.icon} /> <Icon name="comments-outline" class={styles.icon} />
</div> </div>
<a href={getPagePath(router, 'create')} class={styles.control}> <a href={getPagePath(router, 'create')} class={styles.control}>

View File

@ -14,6 +14,7 @@ import { useSession } from '../../context/session'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { useEditorContext } from '../../context/editor'
type HeaderAuthProps = { type HeaderAuthProps = {
setIsProfilePopupVisible: (value: boolean) => void setIsProfilePopupVisible: (value: boolean) => void
@ -27,6 +28,10 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
const { session, isSessionLoaded, isAuthenticated } = useSession() const { session, isSessionLoaded, isAuthenticated } = useSession()
const {
actions: { toggleEditorPanel }
} = useEditorContext()
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings()) const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
const handleBellIconClick = (event: Event) => { const handleBellIconClick = (event: Event) => {
@ -36,7 +41,6 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
showModal('auth') showModal('auth')
return return
} }
toggleWarnings() toggleWarnings()
} }

View File

@ -1,9 +1,8 @@
import { For, Show } from 'solid-js' import { For, Show } from 'solid-js'
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import './Topics.scss'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import './Topics.scss'
export const NavTopics = (props: { topics: Topic[] }) => { export const NavTopics = (props: { topics: Topic[] }) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()

View File

@ -1,5 +1,5 @@
import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffect } from 'solid-js' import { Show, createMemo, createSignal, Switch, onMount, For, Match, createEffect } from 'solid-js'
import type { Author, Shout } from '../../graphql/types.gen' import type { Author, Shout, Topic } from '../../graphql/types.gen'
import { Row1 } from '../Feed/Row1' import { Row1 } from '../Feed/Row1'
import { Row2 } from '../Feed/Row2' import { Row2 } from '../Feed/Row2'
import { AuthorFull } from '../Author/Full' import { AuthorFull } from '../Author/Full'
@ -19,6 +19,7 @@ import { apiClient } from '../../utils/apiClient'
import { Comment } from '../Article/Comment' import { Comment } from '../Article/Comment'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { AuthorRatingControl } from '../Author/AuthorRatingControl' import { AuthorRatingControl } from '../Author/AuthorRatingControl'
import { TopicCard } from '../Topic/Card'
type AuthorProps = { type AuthorProps = {
shouts: Shout[] shouts: Shout[]
@ -27,7 +28,17 @@ type AuthorProps = {
} }
export type AuthorPageSearchParams = { export type AuthorPageSearchParams = {
by: '' | 'viewed' | 'rating' | 'commented' | 'recent' | 'followed' | 'about' | 'popular' by:
| ''
| 'viewed'
| 'rating'
| 'commented'
| 'recent'
| 'subscribed-authors'
| 'subscribed-topics'
| 'followers'
| 'about'
| 'popular'
} }
export const PRERENDERED_ARTICLES_COUNT = 12 export const PRERENDERED_ARTICLES_COUNT = 12
@ -35,35 +46,50 @@ const LOAD_MORE_PAGE_SIZE = 9
export const AuthorView = (props: AuthorProps) => { export const AuthorView = (props: AuthorProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { sortedArticles } = useArticlesStore({ const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
shouts: props.shouts const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
})
const { authorEntities } = useAuthorsStore({ authors: [props.author] }) const { authorEntities } = useAuthorsStore({ authors: [props.author] })
const author = authorEntities()[props.authorSlug]
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const author = createMemo(() => authorEntities()[props.authorSlug])
const [followers, setFollowers] = createSignal<Author[]>([]) const [followers, setFollowers] = createSignal<Author[]>([])
const [followingUsers, setFollowingUsers] = createSignal<Author[]>([])
const [subscribedTopics, setSubscribedTopics] = createSignal<Topic[]>([])
onMount(async () => { onMount(async () => {
try { try {
const authorSubscribers = await apiClient.getAuthorFollowers({ slug: props.author.slug }) const userSubscribers = await apiClient.getAuthorFollowers({ slug: props.authorSlug })
setFollowers(authorSubscribers) setFollowers(userSubscribers)
} catch (error) { } catch (error) {
console.log('[getAuthorSubscribers]', error) console.log('[getAuthorFollowers]', error)
} }
})
const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>() try {
const authorSubscriptionsUsers = await apiClient.getAuthorFollowingUsers({ slug: props.authorSlug })
setFollowingUsers(authorSubscriptionsUsers)
} catch (error) {
console.log('[getAuthorFollowingUsers]', error)
}
try {
const authorSubscriptionsTopics = await apiClient.getAuthorFollowingTopics({ slug: props.authorSlug })
setSubscribedTopics(authorSubscriptionsTopics)
} catch (error) {
console.log('[getAuthorFollowing]', error)
}
onMount(() => {
if (!searchParams().by) { if (!searchParams().by) {
changeSearchParam('by', 'rating') changeSearchParam('by', 'rating')
} }
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
await loadMore()
}
}) })
const loadMore = async () => { const loadMore = async () => {
saveScrollPosition() saveScrollPosition()
const { hasMore } = await loadShouts({ const { hasMore } = await loadShouts({
filters: { author: author().slug }, filters: { author: props.authorSlug },
limit: LOAD_MORE_PAGE_SIZE, limit: LOAD_MORE_PAGE_SIZE,
offset: sortedArticles().length offset: sortedArticles().length
}) })
@ -71,12 +97,6 @@ export const AuthorView = (props: AuthorProps) => {
restoreScrollPosition() restoreScrollPosition()
} }
onMount(async () => {
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
loadMore()
}
})
// TODO: use title // TODO: use title
// const title = createMemo(() => { // const title = createMemo(() => {
// const m = searchParams().by // const m = searchParams().by
@ -91,6 +111,7 @@ export const AuthorView = (props: AuthorProps) => {
) )
const [commented, setCommented] = createSignal([]) const [commented, setCommented] = createSignal([])
createEffect(async () => { createEffect(async () => {
if (searchParams().by === 'commented') { if (searchParams().by === 'commented') {
try { try {
@ -107,7 +128,7 @@ export const AuthorView = (props: AuthorProps) => {
return ( return (
<div class="author-page"> <div class="author-page">
<div class="wide-container"> <div class="wide-container">
<AuthorFull author={author()} /> <AuthorFull author={author} />
<div class="row group__controls"> <div class="row group__controls">
<div class="col-md-16"> <div class="col-md-16">
<ul class="view-switcher"> <ul class="view-switcher">
@ -116,23 +137,26 @@ export const AuthorView = (props: AuthorProps) => {
{t('Publications')} {t('Publications')}
</button> </button>
</li> </li>
<li classList={{ selected: searchParams().by === 'followed' }}> <li classList={{ selected: searchParams().by === 'followers' }}>
<button type="button" onClick={() => changeSearchParam('by', 'followed')}> <button type="button" onClick={() => changeSearchParam('by', 'followers')}>
{t('Followers')} {t('Followers')}
</button> </button>
</li> </li>
<li classList={{ selected: searchParams().by === 'subscribed-authors' }}>
<button type="button" onClick={() => changeSearchParam('by', 'subscribed-authors')}>
{t('Author subscriptions')}
</button>
</li>
<li classList={{ selected: searchParams().by === 'subscribed-topics' }}>
<button type="button" onClick={() => changeSearchParam('by', 'subscribed-topics')}>
{t('Topic subscriptions')}
</button>
</li>
<li classList={{ selected: searchParams().by === 'commented' }}> <li classList={{ selected: searchParams().by === 'commented' }}>
<button type="button" onClick={() => changeSearchParam('by', 'commented')}> <button type="button" onClick={() => changeSearchParam('by', 'commented')}>
{t('Comments')} {t('Comments')}
</button> </button>
</li> </li>
{/*
<li classList={{ selected: searchParams().by === 'popular' }}>
<button type="button" onClick={() => changeSearchParam('by', 'popular')}>
Популярное
</button>
</li>
*/}
<li classList={{ selected: searchParams().by === 'about' }}> <li classList={{ selected: searchParams().by === 'about' }}>
<button type="button" onClick={() => changeSearchParam('by', 'about')}> <button type="button" onClick={() => changeSearchParam('by', 'about')}>
{t('About myself')} {t('About myself')}
@ -197,9 +221,7 @@ export const AuthorView = (props: AuthorProps) => {
> >
<Match when={searchParams().by === 'about'}> <Match when={searchParams().by === 'about'}>
<div class="wide-container"> <div class="wide-container">
<Show when={author().bio}> <p>{author.bio}</p>
<p>{author().bio}</p>
</Show>
</div> </div>
</Match> </Match>
<Match when={searchParams().by === 'commented'}> <Match when={searchParams().by === 'commented'}>
@ -209,7 +231,20 @@ export const AuthorView = (props: AuthorProps) => {
</ul> </ul>
</div> </div>
</Match> </Match>
<Match when={searchParams().by === 'followed'}> <Match when={searchParams().by === 'subscribed-topics'}>
<div class="wide-container">
<div class="row">
<For each={subscribedTopics()}>
{(topic) => (
<div class="col-md-12 col-lg-8">
<TopicCard compact iconButton isTopicInRow topic={topic} />
</div>
)}
</For>
</div>
</div>
</Match>
<Match when={searchParams().by === 'followers'}>
<div class="wide-container"> <div class="wide-container">
<div class="row"> <div class="row">
<For each={followers()}> <For each={followers()}>
@ -222,6 +257,19 @@ export const AuthorView = (props: AuthorProps) => {
</div> </div>
</div> </div>
</Match> </Match>
<Match when={searchParams().by === 'subscribed-authors'}>
<div class="wide-container">
<div class="row">
<For each={followingUsers()}>
{(follower: Author) => (
<div class="col-md-6 col-lg-4">
<AuthorCard author={follower} hideWriteButton={true} hasLink={true} />
</div>
)}
</For>
</div>
</div>
</Match>
<Match when={searchParams().by === 'rating'}> <Match when={searchParams().by === 'rating'}>
<Row1 article={sortedArticles()[0]} /> <Row1 article={sortedArticles()[0]} />
<Row2 articles={sortedArticles().slice(1, 3)} isEqual={true} /> <Row2 articles={sortedArticles().slice(1, 3)} isEqual={true} />

View File

@ -0,0 +1,57 @@
:global(.main-content) {
position: static;
}
.articlePreview {
border: 2px solid #e8e8e8;
min-height: 10em;
padding: 1rem 1.2rem;
}
.formHolder {
padding: 0 4rem;
}
.saveBlock {
background: #f1f1f1;
line-height: 1.4;
margin-top: 6.4rem;
padding: 1.6rem 3.2rem;
text-align: center;
@include media-breakpoint-up(md) {
padding: 3.2rem 8rem;
}
.button {
margin: 0 divide($container-padding-x, 2);
}
}
.container {
.titleInput,
.subtitleInput {
border: 0;
outline: 0;
padding: 0;
font-size: 36px;
&::placeholder {
opacity: 0.3;
color: #000;
}
}
.titleInput {
font-weight: 700;
}
}
.createSettings,
.create {
display: none;
&.visible {
display: block;
}
}

View File

@ -3,7 +3,7 @@ import '../../styles/Feed.scss'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { ArticleCard } from '../Feed/Card' import { ArticleCard } from '../Feed/Card'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/Card'
import { FeedSidebar } from '../Feed/Sidebar' import { Sidebar } from '../Feed/Sidebar'
import { loadShouts, useArticlesStore } from '../../stores/zine/articles' import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
@ -73,18 +73,18 @@ export const FeedView = () => {
onMount(async () => { onMount(async () => {
// load recent shouts not only published ( visibility = community ) // load recent shouts not only published ( visibility = community )
loadMore() await loadMore()
// load 5 recent comments overall // load 5 recent comments overall
const comments = await loadReactionsBy({ by: { comment: true }, limit: 5 }) const comments = await loadReactionsBy({ by: { comment: true }, limit: 5 })
setTopComments(comments) setTopComments(comments)
}) })
return ( return (
<> <div>
<div class="wide-container feed"> <div class="wide-container feed">
<div class="row"> <div class="row">
<div class={clsx('col-md-5 col-xl-4', styles.feedNavigation)}> <div class={clsx('col-md-5 col-xl-4', styles.feedNavigation)}>
<FeedSidebar authors={sortedAuthors()} /> <Sidebar authors={sortedAuthors()} />
</div> </div>
<div class="col-md-12 offset-xl-1"> <div class="col-md-12 offset-xl-1">
@ -198,6 +198,6 @@ export const FeedView = () => {
</aside> </aside>
</div> </div>
</div> </div>
</> </div>
) )
} }

View File

@ -5,7 +5,7 @@ import styles from './Button.module.scss'
type Props = { type Props = {
value: string | JSX.Element value: string | JSX.Element
size?: 'S' | 'M' | 'L' size?: 'S' | 'M' | 'L'
variant?: 'primary' | 'secondary' | 'inline' variant?: 'primary' | 'secondary' | 'inline' | 'outline'
type?: 'submit' | 'button' type?: 'submit' | 'button'
loading?: boolean loading?: boolean
disabled?: boolean disabled?: boolean

41
src/context/editor.tsx Normal file
View File

@ -0,0 +1,41 @@
import type { JSX } from 'solid-js'
import { Accessor, createContext, createSignal, useContext } from 'solid-js'
type WordCounter = {
characters: number
words: number
paragraphs?: number
}
type EditorContextType = {
isEditorPanelVisible: Accessor<boolean>
wordCounter: Accessor<WordCounter>
actions: {
toggleEditorPanel: () => void
countWords: (value: WordCounter) => void
}
}
const EditorContext = createContext<EditorContextType>()
export function useEditorContext() {
return useContext(EditorContext)
}
export const EditorProvider = (props: { children: JSX.Element }) => {
const [isEditorPanelVisible, setEditorPanelVisible] = createSignal<boolean>(false)
const [wordCounter, setWordCounter] = createSignal<WordCounter>({
characters: 0,
words: 0
})
const toggleEditorPanel = () => setEditorPanelVisible(!isEditorPanelVisible())
const countWords = (value) => setWordCounter(value)
const actions = {
toggleEditorPanel,
countWords
}
const value: EditorContextType = { actions, isEditorPanelVisible, wordCounter }
return <EditorContext.Provider value={value}>{props.children}</EditorContext.Provider>
}

View File

@ -5,7 +5,7 @@ export default gql`
createShout(inp: $shout) { createShout(inp: $shout) {
error error
shout { shout {
id _id: slug
slug slug
title title
subtitle subtitle

View File

@ -0,0 +1,19 @@
import { gql } from '@urql/core'
export default gql`
query UserFollowingTopicsQuery($slug: String!) {
userFollowedTopics(slug: $slug) {
id
slug
title
body
pic
# community
stat {
shouts
followers
authors
}
}
}
`

View File

@ -0,0 +1,12 @@
import { gql } from '@urql/core'
export default gql`
query UserFollowingUsersQuery($slug: String!) {
userFollowedAuthors(slug: $slug) {
id
slug
name
userpic
}
}
`

View File

@ -1,23 +0,0 @@
import { gql } from '@urql/core'
export default gql`
query UserFollowingQuery($slug: String!) {
userFollowing(slug: $slug) {
_id: slug
id
slug
name
bio
userpic
communities
links
createdAt
lastSeen
ratings {
_id: rater
rater
value
}
}
}
`

View File

@ -150,9 +150,13 @@ export const ProfileSettingsPage = () => {
<h4>{t('Introduce')}</h4> <h4>{t('Introduce')}</h4>
<div class="pretty-form__item"> <div class="pretty-form__item">
<textarea name="presentation" id="presentation" placeholder={t('Introduce')}> <textarea
{form.bio} name="bio"
</textarea> id="bio"
placeholder={t('Introduce')}
value={form.bio}
onChange={(event) => updateFormField('bio', event.currentTarget.value)}
/>
<label for="presentation">{t('Introduce')}</label> <label for="presentation">{t('Introduce')}</label>
</div> </div>

View File

@ -2,7 +2,6 @@ import type { Accessor } from 'solid-js'
import { createRouter, createSearchParams } from '@nanostores/router' import { createRouter, createSearchParams } from '@nanostores/router'
import { isServer } from 'solid-js/web' import { isServer } from 'solid-js/web'
import { useStore } from '@nanostores/solid' import { useStore } from '@nanostores/solid'
import { getPageLoadManagerPromise } from '../utils/pageLoadManager'
export const ROUTES = { export const ROUTES = {
home: '/', home: '/',
@ -105,33 +104,9 @@ const handleClientRouteLinkClick = async (event) => {
top: 0, top: 0,
left: 0 left: 0
}) })
return return
} }
await getPageLoadManagerPromise()
const images = document.querySelectorAll('img')
let imagesLoaded = 0
const imageLoadEventHandler = () => {
imagesLoaded++
if (imagesLoaded === images.length) {
scrollToHash(url.hash) scrollToHash(url.hash)
images.forEach((image) => image.removeEventListener('load', imageLoadEventHandler))
images.forEach((image) => image.removeEventListener('error', imageLoadEventHandler))
}
}
images.forEach((image) => {
if (image.complete) {
imagesLoaded++
}
image.addEventListener('load', imageLoadEventHandler)
image.addEventListener('error', imageLoadEventHandler)
})
} }
export const initRouter = (pathname: string, search: Record<string, string>) => { export const initRouter = (pathname: string, search: Record<string, string>) => {

View File

@ -2,7 +2,6 @@ import type { FollowingEntity } from '../../graphql/types.gen'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
export const follow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => { export const follow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
console.log('!!! follow:')
await apiClient.follow({ what, slug }) await apiClient.follow({ what, slug })
} }
export const unfollow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => { export const unfollow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {

View File

@ -39,7 +39,8 @@ import myChats from '../graphql/query/chats-load'
import chatMessagesLoadBy from '../graphql/query/chat-messages-load-by' import chatMessagesLoadBy from '../graphql/query/chat-messages-load-by'
import authorBySlug from '../graphql/query/author-by-slug' import authorBySlug from '../graphql/query/author-by-slug'
import userSubscribers from '../graphql/query/author-followers' import userSubscribers from '../graphql/query/author-followers'
import userFollowing from '../graphql/query/author-following' import userFollowedAuthors from '../graphql/query/author-following-users'
import userFollowedTopics from '../graphql/query/author-following-topics'
import topicBySlug from '../graphql/query/topic-by-slug' import topicBySlug from '../graphql/query/topic-by-slug'
import createChat from '../graphql/mutation/create-chat' import createChat from '../graphql/mutation/create-chat'
import reactionsLoadBy from '../graphql/query/reactions-load-by' import reactionsLoadBy from '../graphql/query/reactions-load-by'
@ -224,9 +225,13 @@ export const apiClient = {
const response = await publicGraphQLClient.query(userSubscribers, { slug }).toPromise() const response = await publicGraphQLClient.query(userSubscribers, { slug }).toPromise()
return response.data.userFollowers return response.data.userFollowers
}, },
getAuthorFollowing: async ({ slug }: { slug: string }): Promise<Author[]> => { getAuthorFollowingUsers: async ({ slug }: { slug: string }): Promise<Author[]> => {
const response = await publicGraphQLClient.query(userFollowing, { slug }).toPromise() const response = await publicGraphQLClient.query(userFollowedAuthors, { slug }).toPromise()
return response.data.userFollowing return response.data.userFollowedAuthors
},
getAuthorFollowingTopics: async ({ slug }: { slug: string }): Promise<Topic[]> => {
const response = await publicGraphQLClient.query(userFollowedTopics, { slug }).toPromise()
return response.data.userFollowedTopics
}, },
updateProfile: async (input: ProfileInput) => { updateProfile: async (input: ProfileInput) => {
const response = await privateGraphQLClient.mutation(updateProfile, { profile: input }).toPromise() const response = await privateGraphQLClient.mutation(updateProfile, { profile: input }).toPromise()