This commit is contained in:
Untone 2024-06-25 14:25:20 +03:00
parent bbff52019b
commit bc52dcc653
11 changed files with 21 additions and 547 deletions

View File

@ -1,10 +1,11 @@
import { MetaProvider } from '@solidjs/meta' import { Meta, MetaProvider } from '@solidjs/meta'
import { Router } from '@solidjs/router' import { Router } from '@solidjs/router'
import { FileRoutes } from '@solidjs/start/router' import { FileRoutes } from '@solidjs/start/router'
import { type JSX, Suspense } from 'solid-js' import { type JSX, Suspense } from 'solid-js'
import { Loading } from './components/_shared/Loading' import { Loading } from './components/_shared/Loading'
import { PageLayout } from './components/_shared/PageLayout' import { PageLayout } from './components/_shared/PageLayout'
import { EditorProvider } from './context/editor'
import { FeedProvider } from './context/feed' import { FeedProvider } from './context/feed'
import { GraphQLClientProvider } from './context/graphql' import { GraphQLClientProvider } from './context/graphql'
import { LocalizeProvider, useLocalize } from './context/localize' import { LocalizeProvider, useLocalize } from './context/localize'
@ -22,10 +23,13 @@ export const Providers = (props: { children?: JSX.Element }) => {
<TopicsProvider> <TopicsProvider>
<FeedProvider> <FeedProvider>
<MetaProvider> <MetaProvider>
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<UIProvider> <UIProvider>
<EditorProvider>
<Suspense fallback={<Loading />}> <Suspense fallback={<Loading />}>
<PageLayout title={t('Discours')}>{props.children}</PageLayout> <PageLayout title={t('Discours')}>{props.children}</PageLayout>
</Suspense> </Suspense>
</EditorProvider>
</UIProvider> </UIProvider>
</MetaProvider> </MetaProvider>
</FeedProvider> </FeedProvider>

View File

@ -1,142 +0,0 @@
import type { PageProps, RootSearchParams } from '../pages/types'
import { Component, createEffect, createMemo } from 'solid-js'
import { Dynamic } from 'solid-js/web'
import { Meta, MetaProvider } from '../context/meta'
import { ConfirmProvider } from '../context/confirm'
import { ConnectProvider } from '../context/connect'
import { EditorProvider } from '../context/editor'
import { FollowingProvider } from '../context/following'
import { InboxProvider } from '../context/inbox'
import { LocalizeProvider } from '../context/localize'
import { MediaQueryProvider } from '../context/mediaQuery'
import { NotificationsProvider } from '../context/notifications'
import { SeenProvider } from '../context/seen'
import { SessionProvider } from '../context/session'
import { SnackbarProvider } from '../context/snackbar'
import { TopicsProvider } from '../context/topics'
import { DiscussionRulesPage } from '../pages/about/discussionRules.page'
import { DogmaPage } from '../pages/about/dogma.page'
import { GuidePage } from '../pages/about/guide.page'
import { HelpPage } from '../pages/about/help.page'
import { ManifestPage } from '../pages/about/manifest.page'
import { PartnersPage } from '../pages/about/partners.page'
import { PrinciplesPage } from '../pages/about/principles.page'
import { ProjectsPage } from '../pages/about/projects.page'
import { TermsOfUsePage } from '../pages/about/termsOfUse.page'
import { ThanksPage } from '../pages/about/thanks.page'
import { AllAuthorsPage } from '../pages/allAuthors.page'
import { AllTopicsPage } from '../pages/allTopics.page'
import { ArticlePage } from '../pages/article.page'
import { AuthorPage } from '../pages/author.page'
import { ConnectPage } from '../pages/connect.page'
import { CreatePage } from '../pages/create.page'
import { DraftsPage } from '../pages/drafts.page'
import { EditPage } from '../pages/edit.page'
import { ExpoPage } from '../pages/expo/expo.page'
import { FeedPage } from '../pages/feed.page'
import { FourOuFourPage } from '../pages/fourOuFour.page'
import { InboxPage } from '../pages/inbox.page'
import { HomePage } from '../pages/index.page'
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
import { SearchPage } from '../pages/search.page'
import { TopicPage } from '../pages/topic.page'
import { ROUTES, useRouter } from '../stores/router'
import { MODALS, showModal } from '../stores/ui'
const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
author: AuthorPage,
authorComments: AuthorPage,
authorAbout: AuthorPage,
inbox: InboxPage,
expo: ExpoPage,
connect: ConnectPage,
create: CreatePage,
edit: EditPage,
editSettings: EditPage,
drafts: DraftsPage,
home: HomePage,
topics: AllTopicsPage,
topic: TopicPage,
authors: AllAuthorsPage,
feed: FeedPage,
feedMy: FeedPage,
feedNotifications: FeedPage,
feedBookmarks: FeedPage,
feedCollaborations: FeedPage,
feedDiscussions: FeedPage,
article: ArticlePage,
search: SearchPage,
discussionRules: DiscussionRulesPage,
dogma: DogmaPage,
guide: GuidePage,
help: HelpPage,
manifest: ManifestPage,
projects: ProjectsPage,
partners: PartnersPage,
principles: PrinciplesPage,
termsOfUse: TermsOfUsePage,
thanks: ThanksPage,
profileSettings: ProfileSettingsPage,
profileSecurity: ProfileSecurityPage,
profileSubscriptions: ProfileSubscriptionsPage,
fourOuFour: FourOuFourPage,
}
type Props = PageProps & { is404: boolean }
export const App = (props: Props) => {
const { page, searchParams } = useRouter<RootSearchParams>()
const is404 = createMemo(() => props.is404)
createEffect(() => {
const modal = MODALS[searchParams().m]
if (modal) {
showModal(modal)
}
})
const pageComponent = createMemo(() => {
const result = pagesMap[page()?.route || 'home']
if (is404() || !result || page()?.path === '/404') {
return FourOuFourPage
}
return result
})
return (
<MetaProvider>
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<LocalizeProvider>
<MediaQueryProvider>
<SnackbarProvider>
<TopicsProvider>
<SeenProvider>
<ConfirmProvider>
<SessionProvider onStateChangeCallback={console.log}>
<FollowingProvider>
<ConnectProvider>
<NotificationsProvider>
<EditorProvider>
<InboxProvider>
<Dynamic component={pageComponent()} {...props} />
</InboxProvider>
</EditorProvider>
</NotificationsProvider>
</ConnectProvider>
</FollowingProvider>
</SessionProvider>
</ConfirmProvider>
</SeenProvider>
</TopicsProvider>
</SnackbarProvider>
</MediaQueryProvider>
</LocalizeProvider>
</MetaProvider>
)
}

View File

@ -1,9 +0,0 @@
.TopicSelect .solid-select-list {
background: #fff;
position: relative;
z-index: 13;
}
.TopicSelect .solid-select-option[data-disabled='true'] {
display: none;
}

View File

@ -183,13 +183,19 @@ export const UIProvider = (props: { children: JSX.Element }) => {
setModal('') setModal('')
} }
const [searchParams] = useSearchParams()
createEffect( createEffect(
on( on(
modal, [modal, () => searchParams?.m || ''],
(m) => { ([m1, m2]) => {
if (m) showModal(m) const m = m1 || m2 || ''
setModal((_) => m as ModalType)
if (m) {
showModal(m as ModalType)
}
}, },
{}, { defer: true },
), ),
) )

View File

@ -1,74 +0,0 @@
import { chatApiUrl } from '../../utils/config'
// inbox
import { createGraphQLClient } from '../createGraphQLClient'
import createChat from '../mutation/chat/chat-create'
import deleteChat from '../mutation/chat/chat-delete'
import markAsRead from '../mutation/chat/chat-mark-as-read'
import createChatMessage from '../mutation/chat/chat-message-create'
import deleteChatMessage from '../mutation/chat/chat-message-delete'
import updateChatMessage from '../mutation/chat/chat-message-update'
import updateChat from '../mutation/chat/chat-update'
import chatMessagesLoadBy from '../query/chat/chat-messages-load-by'
import myChats from '../query/chat/chats-load'
import {
Chat,
MutationCreate_ChatArgs,
MutationCreate_MessageArgs,
MutationDelete_ChatArgs,
MutationDelete_MessageArgs,
MutationMark_As_ReadArgs,
MutationUpdate_ChatArgs,
MutationUpdate_MessageArgs,
QueryLoad_ChatsArgs,
QueryLoad_Messages_ByArgs,
} from '../schema/chat.gen'
export const inboxClient = {
private: null,
connect: (token: string) => (inboxClient.private = createGraphQLClient(chatApiUrl, token)),
loadChats: async (options: QueryLoad_ChatsArgs): Promise<Chat[]> => {
const resp = await inboxClient.private.query(myChats, options).toPromise()
return resp.data.load_chats.chats
},
createChat: async (options: MutationCreate_ChatArgs) => {
const resp = await inboxClient.private.mutation(createChat, options).toPromise()
return resp.data.create_chat
},
markAsRead: async (options: MutationMark_As_ReadArgs) => {
const resp = await inboxClient.private.mutation(markAsRead, options).toPromise()
return resp.data.mark_as_read
},
updateChat: async (options: MutationUpdate_ChatArgs) => {
const resp = await inboxClient.private.mutation(updateChat, options).toPromise()
return resp.data.update_chat
},
deleteChat: async (options: MutationDelete_ChatArgs) => {
const resp = await inboxClient.private.mutation(deleteChat, options).toPromise()
return resp.data.delete_chat
},
createMessage: async (options: MutationCreate_MessageArgs) => {
const resp = await inboxClient.private.mutation(createChatMessage, options).toPromise()
return resp.data.create_message.message
},
updateMessage: async (options: MutationUpdate_MessageArgs) => {
const resp = await inboxClient.private.mutation(updateChatMessage, options).toPromise()
return resp.data.update_message.message
},
deleteMessage: async (options: MutationDelete_MessageArgs) => {
const resp = await inboxClient.private.mutation(deleteChatMessage, options).toPromise()
return resp.data.delete_message
},
loadChatMessages: async (options: QueryLoad_Messages_ByArgs) => {
const resp = await inboxClient.private.query(chatMessagesLoadBy, options).toPromise()
return resp.data.load_messages_by.messages
},
}

View File

@ -1,244 +0,0 @@
import type {
Author,
CommonResult,
FollowingEntity,
LoadShoutsOptions,
MutationDelete_ShoutArgs,
ProfileInput,
QueryGet_Topic_FollowersArgs,
QueryLoad_Authors_ByArgs,
QueryLoad_Shouts_Random_TopArgs,
QueryLoad_Shouts_SearchArgs,
ReactionBy,
ReactionInput,
Shout,
ShoutInput,
Topic,
} from '../schema/core.gen'
import { coreApiUrl } from '../../utils/config'
import { createGraphQLClient } from '../createGraphQLClient'
import createArticle from '../mutation/core/article-create'
import deleteShout from '../mutation/core/article-delete'
import updateArticle from '../mutation/core/article-update'
import rateAuthor from '../mutation/core/author-rate'
import updateAuthor from '../mutation/core/author-update'
import followMutation from '../mutation/core/follow'
import reactionCreate from '../mutation/core/reaction-create'
import reactionDestroy from '../mutation/core/reaction-destroy'
import reactionUpdate from '../mutation/core/reaction-update'
import unfollowMutation from '../mutation/core/unfollow'
import shoutLoad from '../query/core/article-load'
import getMyShout from '../query/core/article-my'
import shoutsLoadBy from '../query/core/articles-load-by'
import draftsLoad from '../query/core/articles-load-drafts'
import myFeed from '../query/core/articles-load-feed'
import loadShoutsTopRandom from '../query/core/articles-load-random-top'
import articlesLoadRandomTopic from '../query/core/articles-load-random-topic'
import shoutsLoadSearch from '../query/core/articles-load-search'
import loadShoutsUnrated from '../query/core/articles-load-unrated'
import authorBy from '../query/core/author-by'
import authorFollowers from '../query/core/author-followers'
import authorFollows from '../query/core/author-follows'
import authorsAll from '../query/core/authors-all'
import authorsLoadBy from '../query/core/authors-load-by'
import reactionsLoadBy from '../query/core/reactions-load-by'
import topicBySlug from '../query/core/topic-by-slug'
import topicFollowers from '../query/core/topic-followers'
import topicsAll from '../query/core/topics-all'
import topicsRandomQuery from '../query/core/topics-random'
const publicGraphQLClient = createGraphQLClient(coreApiUrl)
export const apiClient = {
private: null,
connect: (token: string) => {
// NOTE: use it after token appears
apiClient.private = createGraphQLClient(coreApiUrl, token)
},
getRandomTopShouts: async (params: QueryLoad_Shouts_Random_TopArgs) => {
const response = await publicGraphQLClient.query(loadShoutsTopRandom, params).toPromise()
if (!response.data) console.error('[graphql.core] load_shouts_random_top failed', response)
return response.data.load_shouts_random_top
},
getUnratedShouts: async (limit = 50, offset = 0) => {
const response = await apiClient.private.query(loadShoutsUnrated, { limit, offset }).toPromise()
if (!response.data) console.error('[graphql.core] load_shouts_unrated', response)
return response.data.load_shouts_unrated
},
rateAuthor: async ({ rated_slug, value }: { rated_slug: string; value: number }) => {
const response = await apiClient.private.mutation(rateAuthor, { rated_slug, value }).toPromise()
if (!response.data) console.error('[graphql.client.core] get_topics_random failed', response)
return response.data.rate_author
},
getRandomTopics: async ({ amount }: { amount: number }) => {
const response = await publicGraphQLClient.query(topicsRandomQuery, { amount }).toPromise()
if (!response.data) console.error('[graphql.client.core] get_topics_random failed', response)
return response.data.get_topics_random
},
getRandomTopicShouts: async (limit: number): Promise<CommonResult> => {
const resp = await publicGraphQLClient.query(articlesLoadRandomTopic, { limit }).toPromise()
if (!resp.data) console.error('[graphql.client.core] load_shouts_random_topic', resp)
return resp.data.load_shouts_random_topic
},
follow: async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
const response = await apiClient.private.mutation(followMutation, { what, slug }).toPromise()
return response.data.follow
},
unfollow: async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
const response = await apiClient.private.mutation(unfollowMutation, { what, slug }).toPromise()
return response.data.unfollow
},
getAllTopics: async () => {
const response = await publicGraphQLClient.query(topicsAll, {}).toPromise()
if (!response.data) console.error('[graphql.client.core] get_topics_all', response)
return response.data.get_topics_all
},
getAllAuthors: async () => {
const response = await publicGraphQLClient.query(authorsAll, {}).toPromise()
if (!response.data) console.error('[graphql.client.core] getAllAuthors', response)
return response.data.get_authors_all
},
getAuthor: async (params: { slug?: string; author_id?: number }): Promise<Author> => {
const response = await publicGraphQLClient.query(authorBy, params).toPromise()
return response.data.get_author
},
getAuthorFollowers: async ({ slug }: { slug: string }): Promise<Author[]> => {
const response = await publicGraphQLClient.query(authorFollowers, { slug }).toPromise()
return response.data.get_author_followers
},
getTopicFollowers: async ({ slug }: QueryGet_Topic_FollowersArgs): Promise<Author[]> => {
const response = await publicGraphQLClient.query(topicFollowers, { slug }).toPromise()
return response.data.get_topic_followers
},
getAuthorFollows: async (params: {
slug?: string
author_id?: number
user?: string
}): Promise<CommonResult> => {
const response = await publicGraphQLClient.query(authorFollows, params).toPromise()
return response.data.get_author_follows
},
updateAuthor: async (input: ProfileInput) => {
const response = await apiClient.private.mutation(updateAuthor, { profile: input }).toPromise()
return response.data.update_author
},
getTopic: async ({ slug }: { slug: string }): Promise<Topic> => {
const response = await publicGraphQLClient.query(topicBySlug, { slug }).toPromise()
return response.data.get_topic
},
createArticle: async ({ article }: { article: ShoutInput }): Promise<Shout> => {
const response = await apiClient.private.mutation(createArticle, { shout: article }).toPromise()
return response.data.create_shout.shout
},
updateArticle: async ({
shout_id,
shout_input,
publish,
}: {
shout_id: number
shout_input?: ShoutInput
publish: boolean
}): Promise<CommonResult> => {
const response = await apiClient.private
.mutation(updateArticle, { shout_id, shout_input, publish })
.toPromise()
console.debug('[graphql.client.core] updateArticle:', response.data)
return response.data.update_shout
},
deleteShout: async (params: MutationDelete_ShoutArgs): Promise<void> => {
const response = await apiClient.private.mutation(deleteShout, params).toPromise()
console.debug('[graphql.client.core] deleteShout:', response)
},
getDrafts: async (): Promise<CommonResult> => {
const response = await apiClient.private.query(draftsLoad, {}).toPromise()
console.debug('[graphql.client.core] getDrafts:', response)
return response.data.get_shouts_drafts
},
createReaction: async (input: ReactionInput) => {
const response = await apiClient.private.mutation(reactionCreate, { reaction: input }).toPromise()
console.debug('[graphql.client.core] createReaction:', response)
return response.data.create_reaction
},
destroyReaction: async (reaction_id: number) => {
const response = await apiClient.private.mutation(reactionDestroy, { reaction_id }).toPromise()
console.debug('[graphql.client.core] destroyReaction:', response)
return response.data.delete_reaction
},
updateReaction: async (reaction: ReactionInput) => {
const response = await apiClient.private.mutation(reactionUpdate, { reaction }).toPromise()
console.debug('[graphql.client.core] updateReaction:', response)
return response.data.update_reaction
},
loadAuthorsBy: async (args: QueryLoad_Authors_ByArgs) => {
const resp = await publicGraphQLClient.query(authorsLoadBy, args).toPromise()
console.debug('[graphql.client.core] authorsLoadBy:', resp)
return resp.data.load_authors_by
},
getShoutBySlug: async (slug: string) => {
const resp = await publicGraphQLClient.query(shoutLoad, { slug }).toPromise()
return resp.data.get_shout
},
getMyShout: async (shout_id: number) => {
await apiClient.private
const resp = await apiClient.private.query(getMyShout, { shout_id }).toPromise()
if (resp.error) console.error(resp)
return resp.data.get_my_shout
},
getShouts: async (options: LoadShoutsOptions) => {
const resp = await publicGraphQLClient.query(shoutsLoadBy, { options }).toPromise()
if (resp.error) console.error(resp)
return resp.data?.load_shouts_by
},
getShoutsSearch: async ({ text, limit, offset }: QueryLoad_Shouts_SearchArgs) => {
const resp = await publicGraphQLClient.query(shoutsLoadSearch, { text, limit, offset }).toPromise()
if (resp.error) console.error(resp)
return resp.data.load_shouts_search
},
getMyFeed: async (options: LoadShoutsOptions) => {
const resp = await apiClient.private.query(myFeed, { options }).toPromise()
if (resp.error) console.error(resp)
return resp.data.load_shouts_feed
},
getReactionsBy: async ({ by, limit, offset }: { by: ReactionBy; limit?: number; offset?: number }) => {
const resp = await publicGraphQLClient
.query(reactionsLoadBy, { by, limit: limit ?? 1000, offset: offset ?? 0 })
.toPromise()
return resp.data.load_reactions_by
},
}

View File

@ -1,32 +0,0 @@
import markSeenMutation from '../mutation/notifier/mark-seen'
import markSeenAfterMutation from '../mutation/notifier/mark-seen-after'
import markThreadSeenMutation from '../mutation/notifier/mark-seen-thread'
import loadNotifications from '../query/notifier/notifications-load'
import {
MutationNotifications_Seen_AfterArgs,
NotificationsResult,
QueryLoad_NotificationsArgs,
} from '../schema/core.gen'
import { apiClient } from './core'
export const notifierClient = {
private: apiClient.private,
getNotifications: async (params: QueryLoad_NotificationsArgs): Promise<NotificationsResult> => {
const resp = await notifierClient.private.query(loadNotifications, params).toPromise()
return resp.data?.load_notifications
},
markSeen: async (notification_id: number): Promise<void> => {
// call when notification is clicked
await notifierClient.private.mutation(markSeenMutation, { notification_id }).toPromise()
},
markSeenAfter: async (options: MutationNotifications_Seen_AfterArgs): Promise<void> => {
// call when 'mark all as seen' cliecked
await notifierClient.private.mutation(markSeenAfterMutation, options).toPromise()
},
markSeenThread: async (thread: string): Promise<void> => {
// call when notification group is clicked
await notifierClient.private.mutation(markThreadSeenMutation, { thread }).toPromise()
},
}

View File

@ -1,20 +0,0 @@
import { ClientOptions, Exchange, createClient, fetchExchange } from '@urql/core'
import { devtoolsExchange } from '@urql/devtools'
import { isDev } from '../utils/config'
const exchanges: Exchange[] = [fetchExchange]
if (isDev) {
exchanges.unshift(devtoolsExchange)
}
export const createGraphQLClient = (url: string, token = '') => {
const options: ClientOptions = {
url,
requestPolicy: 'cache-and-network',
fetchOptions: () => (token ? { headers: { Authorization: token } } : {}),
exchanges,
}
return createClient(options)
}

View File

@ -1,16 +0,0 @@
type ApiErrorCode =
| 'unknown'
| 'email_not_confirmed'
| 'user_not_found'
| 'user_already_exists'
| 'token_expired'
| 'token_invalid'
export class ApiError extends Error {
code: ApiErrorCode
constructor(code: ApiErrorCode, message?: string) {
super(message)
this.code = code
}
}

View File

@ -2,7 +2,7 @@ import { expect, test } from '@playwright/test'
const baseHost = process.env.BASE_HOST || 'https://localhost:3000' const baseHost = process.env.BASE_HOST || 'https://localhost:3000'
const pagesTitles = { const pagesTitles: {[key:string]: RegExp } = {
'/': /Дискурс/, '/': /Дискурс/,
'/feed': /Лента/, '/feed': /Лента/,
'/create': /Выберите тип публикации/, '/create': /Выберите тип публикации/,

View File

@ -21,5 +21,6 @@
"~/*": ["./src/*"], "~/*": ["./src/*"],
"@/*": ["./public/*"] "@/*": ["./public/*"]
} }
} },
"exclude": ["./src/pages"]
} }