diff --git a/package-lock.json b/package-lock.json index 1515c15..5832519 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16229,9 +16229,9 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "query-string": { - "version": "6.13.6", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.13.6.tgz", - "integrity": "sha512-/WWZ7d9na6s2wMEGdVCVgKWE9Rt7nYyNIf7k8xmHXcesPMlEzicWo3lbYwHyA4wBktI2KrXxxZeACLbE84hvSQ==", + "version": "6.13.7", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.13.7.tgz", + "integrity": "sha512-CsGs8ZYb39zu0WLkeOhe0NMePqgYdAuCqxOYKDR5LVCytDZYMGx3Bb+xypvQvPHVPijRXB0HZNFllCzHRe4gEA==", "requires": { "decode-uri-component": "^0.2.0", "split-on-first": "^1.0.0", @@ -18222,6 +18222,11 @@ "randombytes": "^2.1.0" } }, + "serialize-query-params": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-1.2.4.tgz", + "integrity": "sha512-m4hGkOY5y+ksPDSEkw12cNxt3HRUJv5G6oF9/4yq+GCw4LznudxC73qnz++VTHqXa0j1x1/iaBIpoiMBxr6w2w==" + }, "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -20674,6 +20679,14 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-query-params": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-1.1.9.tgz", + "integrity": "sha512-WAJ1GrKbFWv1TBn1RQpHqAwC7yyJsLaJjBhIfefrbY/h6mFSngzBQKirJndYwCS1ry77EwhpR/tQi5iovXWvuw==", + "requires": { + "serialize-query-params": "^1.2.3" + } + }, "utf-8-validate": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.2.tgz", diff --git a/package.json b/package.json index ae66be2..5d76814 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,15 @@ "graphql": "^15.3.0", "node-sass": "^4.14.1", "normalize.css": "^8.0.1", - "query-string": "^6.13.6", + "query-string": "^6.13.7", "react": "^16.14.0", "react-dom": "^16.14.0", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-scripts": "3.4.3", "tailwindcss": "^1.9.5", - "typescript": "^3.7.5" + "typescript": "^3.7.5", + "use-query-params": "^1.1.9" }, "scripts": { "start": "react-scripts start", diff --git a/src/app/header/index.tsx b/src/app/header/index.tsx index e8e8c22..8ed8ddb 100644 --- a/src/app/header/index.tsx +++ b/src/app/header/index.tsx @@ -1,11 +1,16 @@ import React from "react"; import { Layout, Input } from "antd"; +import * as qs from "query-string"; import { Auth } from "features"; import ImgLogo from "./logo.png"; import "./index.scss"; const Header = () => { const { isAuth } = Auth.useAuth(); + // !!! FIXME: limit scope of query-params literals + // TODO: (wrap in QueryParamProvider) - wrap app with header instead of only content? + // const [search] = useQueryParam("q", StringParam); + const search = (qs.parse(window.location.search).q as string) || ""; return ( @@ -15,7 +20,20 @@ const Header = () => { logo {!isAuth && GITHUB-CLIENT} - {isAuth && } + {isAuth && ( + { + // @ts-ignore FIXME: specify types + if (key === "Enter" && target.value) { + // @ts-ignore FIXME: specify types + window.location.replace(`/search?q=${target.value}`); + } + }} + /> + )} diff --git a/src/app/index.scss b/src/app/index.scss index 894a898..f6e2283 100644 --- a/src/app/index.scss +++ b/src/app/index.scss @@ -2,6 +2,7 @@ @import "~tailwindcss/dist/utilities.css"; @import "./vars.scss"; @import "./normalize.scss"; +@import "./utils.scss"; .gc-app { display: flex; diff --git a/src/app/utils.scss b/src/app/utils.scss new file mode 100644 index 0000000..0292b1e --- /dev/null +++ b/src/app/utils.scss @@ -0,0 +1,5 @@ +.text-title { + font-family: var(--ff-secondary); + font-size: 20px; + font-weight: var(--fw--medium); +} diff --git a/src/features/auth/router/index.tsx b/src/features/auth/router/index.tsx index 27cb192..dbaa195 100644 --- a/src/features/auth/router/index.tsx +++ b/src/features/auth/router/index.tsx @@ -1,5 +1,6 @@ import React, { lazy, Suspense } from "react"; import { BrowserRouter, Route, Switch } from "react-router-dom"; +import { QueryParamProvider } from "use-query-params"; import { Spin } from "antd"; import { useAuth } from "../hooks"; @@ -16,16 +17,19 @@ const Router = ({ children }: Props) => { return ( }> - - {isAuth && children} - {!isAuth && ( - <> - - - {/* */} - - )} - + {/* FIXME: wrap not on this level */} + + + {isAuth && children} + {!isAuth && ( + <> + + + {/* */} + + )} + + ); diff --git a/src/features/repo-list/index.tsx b/src/features/repo-list/index.tsx index 9f47569..79cc230 100644 --- a/src/features/repo-list/index.tsx +++ b/src/features/repo-list/index.tsx @@ -1,13 +1,14 @@ import React from "react"; +import { Repo, Tabs } from "shared/components"; import { useReposQuery } from "./queries.gen"; -import Tab from "./tab"; -import RepoItem from "./repo-item"; import "./index.scss"; type Props = { username: string; }; +// FIXME: rename to UserRepoList? (coz - user as dep) + const RepoList = ({ username }: Props) => { const { data } = useReposQuery({ variables: { login: username }, @@ -15,13 +16,13 @@ const RepoList = ({ username }: Props) => { return (
-
- -
+ + +
- {data?.user?.repositories.edges?.map((edge) => ( + {data?.user?.repositories.edges?.map((edge, index) => ( // FIXME: destruct more elegant later - + ))}
diff --git a/src/features/repo-list/repo-item/index.tsx b/src/features/repo-list/repo-item/index.tsx deleted file mode 100644 index f54d441..0000000 --- a/src/features/repo-list/repo-item/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import dayjs from "dayjs"; -import FavBtn from "./fav-btn"; -import "./index.scss"; - -// !!! FIXME: specify types -type Props = any; - -const RepoItem = (props: Props) => { - const { name, primaryLanguage, updatedAt, url, viewerHasStarred } = props; - const hex = primaryLanguage?.color; - - return ( -
-
- {/* FIXME: hardcoded, replace to generation by {username}/{reponame} */} - {name} -
- {primaryLanguage?.name} - Updated on {dayjs(updatedAt).format("D MMM YYYY")} -
-
- -
- ); -}; - -export default RepoItem; diff --git a/src/features/repo-list/tab/index.scss b/src/features/repo-list/tab/index.scss deleted file mode 100644 index ffd08f5..0000000 --- a/src/features/repo-list/tab/index.scss +++ /dev/null @@ -1,15 +0,0 @@ -.tab { - padding: 13px 21px; - font-family: var(--ff-primary); - font-size: 19px; - line-height: 33px; - color: #000000; - background: linear-gradient(270deg, rgba(51, 153, 255, 0) 8.76%, rgba(51, 153, 255, 0.33) 100%); - border: none; - border-radius: 5px; - outline-style: none; - - &:hover { - cursor: pointer; - } -} diff --git a/src/features/repo-list/tab/index.tsx b/src/features/repo-list/tab/index.tsx deleted file mode 100644 index 5be800f..0000000 --- a/src/features/repo-list/tab/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import "./index.scss"; - -type Props = { - name: string; -}; - -const Tab = (props: Props) => { - return ; -}; - -export default Tab; diff --git a/src/models.gen.ts b/src/models.gen.ts index c4d13af..2a3a937 100644 --- a/src/models.gen.ts +++ b/src/models.gen.ts @@ -5188,6 +5188,7 @@ export type IssueAssigneesArgs = { /** An Issue is a place to discuss ideas, enhancements, tasks, and bugs for a project. */ export type IssueCommentsArgs = { + orderBy?: Maybe; after?: Maybe; before?: Maybe; first?: Maybe; @@ -5377,6 +5378,20 @@ export type IssueCommentEdge = { readonly node?: Maybe; }; +/** Ways in which lists of issue comments can be ordered upon return. */ +export type IssueCommentOrder = { + /** The field in which to order issue comments by. */ + readonly field: IssueCommentOrderField; + /** The direction in which to order issue comments by the specified field. */ + readonly direction: OrderDirection; +}; + +/** Properties by which issue comment connections can be ordered. */ +export enum IssueCommentOrderField { + /** Order issue comments by update time */ + UpdatedAt = 'UPDATED_AT' +} + /** The connection type for Issue. */ export type IssueConnection = { /** A list of edges. */ @@ -8945,11 +8960,17 @@ export type Organization = Node & Actor & PackageOwner & ProjectOwner & Reposito readonly descriptionHTML?: Maybe; /** The organization's public email. */ readonly email?: Maybe; + /** True if this user/organization has a GitHub Sponsors listing. */ + readonly hasSponsorsListing: Scalars['Boolean']; readonly id: Scalars['ID']; + /** The interaction ability settings for this organization. */ + readonly interactionAbility?: Maybe; /** The setting value for whether the organization has an IP allow list enabled. */ readonly ipAllowListEnabledSetting: IpAllowListEnabledSettingValue; /** The IP addresses that are allowed to access resources owned by the organization. */ readonly ipAllowListEntries: IpAllowListEntryConnection; + /** True if the viewer is sponsored by this user/organization. */ + readonly isSponsoringViewer: Scalars['Boolean']; /** Whether the organization has verified its profile email and website, always false on Enterprise. */ readonly isVerified: Scalars['Boolean']; /** Showcases a selection of repositories and gists that the profile owner has either curated or that have been selected automatically based on popularity. */ @@ -8998,7 +9019,7 @@ export type Organization = Node & Actor & PackageOwner & ProjectOwner & Reposito readonly resourcePath: Scalars['URI']; /** The Organization's SAML identity providers */ readonly samlIdentityProvider?: Maybe; - /** The GitHub Sponsors listing for this user. */ + /** The GitHub Sponsors listing for this user or organization. */ readonly sponsorsListing?: Maybe; /** This object's sponsorships as the maintainer. */ readonly sponsorshipsAsMaintainer: SponsorshipConnection; @@ -9028,8 +9049,12 @@ export type Organization = Node & Actor & PackageOwner & ProjectOwner & Reposito readonly viewerCanCreateRepositories: Scalars['Boolean']; /** Viewer can create teams on this organization. */ readonly viewerCanCreateTeams: Scalars['Boolean']; + /** Whether or not the viewer is able to sponsor this user/organization. */ + readonly viewerCanSponsor: Scalars['Boolean']; /** Viewer is an active member of this organization. */ readonly viewerIsAMember: Scalars['Boolean']; + /** True if the viewer is sponsoring this user/organization. */ + readonly viewerIsSponsoring: Scalars['Boolean']; /** The organization's public profile URL. */ readonly websiteUrl?: Maybe; }; @@ -10488,6 +10513,7 @@ export type PullRequestAssigneesArgs = { /** A repository pull request. */ export type PullRequestCommentsArgs = { + orderBy?: Maybe; after?: Maybe; before?: Maybe; first?: Maybe; @@ -13362,6 +13388,8 @@ export type Repository = Node & ProjectOwner & PackageOwner & Subscribable & Sta /** The repository's URL. */ readonly homepageUrl?: Maybe; readonly id: Scalars['ID']; + /** The interaction ability settings for this repository. */ + readonly interactionAbility?: Maybe; /** Indicates if the repository is unmaintained. */ readonly isArchived: Scalars['Boolean']; /** Returns true if blank issue creation is allowed */ @@ -13963,6 +13991,38 @@ export type RepositoryInfoShortDescriptionHtmlArgs = { limit?: Maybe; }; +/** Repository interaction limit that applies to this object. */ +export type RepositoryInteractionAbility = { + /** The time the currently active limit expires. */ + readonly expiresAt?: Maybe; + /** The current limit that is enabled on this object. */ + readonly limit: RepositoryInteractionLimit; + /** The origin of the currently active interaction limit. */ + readonly origin: RepositoryInteractionLimitOrigin; +}; + +/** A repository interaction limit. */ +export enum RepositoryInteractionLimit { + /** Users that have recently created their account will be unable to interact with the repository. */ + ExistingUsers = 'EXISTING_USERS', + /** Users that have not previously committed to a repository’s default branch will be unable to interact with the repository. */ + ContributorsOnly = 'CONTRIBUTORS_ONLY', + /** Users that are not collaborators will not be able to interact with the repository. */ + CollaboratorsOnly = 'COLLABORATORS_ONLY', + /** No interaction limits are enabled. */ + NoLimit = 'NO_LIMIT' +} + +/** Indicates where an interaction limit is configured. */ +export enum RepositoryInteractionLimitOrigin { + /** A limit that is configured at the repository level. */ + Repository = 'REPOSITORY', + /** A limit that is configured at the organization level. */ + Organization = 'ORGANIZATION', + /** A limit that is configured at the user-wide level. */ + User = 'USER' +} + /** An invitation for a user to be added to a repository. */ export type RepositoryInvitation = Node & { /** The email address that received the invitation. */ @@ -14895,12 +14955,20 @@ export type Sponsor = Organization | User; /** Entities that can be sponsored through GitHub Sponsors */ export type Sponsorable = { - /** The GitHub Sponsors listing for this user. */ + /** True if this user/organization has a GitHub Sponsors listing. */ + readonly hasSponsorsListing: Scalars['Boolean']; + /** True if the viewer is sponsored by this user/organization. */ + readonly isSponsoringViewer: Scalars['Boolean']; + /** The GitHub Sponsors listing for this user or organization. */ readonly sponsorsListing?: Maybe; /** This object's sponsorships as the maintainer. */ readonly sponsorshipsAsMaintainer: SponsorshipConnection; /** This object's sponsorships as the sponsor. */ readonly sponsorshipsAsSponsor: SponsorshipConnection; + /** Whether or not the viewer is able to sponsor this user/organization. */ + readonly viewerCanSponsor: Scalars['Boolean']; + /** True if the viewer is sponsoring this user/organization. */ + readonly viewerIsSponsoring: Scalars['Boolean']; }; @@ -15046,7 +15114,7 @@ export type Sponsorship = Node & { * @deprecated `Sponsorship.sponsor` will be removed. Use `Sponsorship.sponsorEntity` instead. Removal on 2020-10-01 UTC. */ readonly sponsor?: Maybe; - /** The user or organization that is sponsoring. Returns null if the sponsorship is private. */ + /** The user or organization that is sponsoring, if you have permission to view them. */ readonly sponsorEntity?: Maybe; /** The entity that is being sponsored */ readonly sponsorable: Sponsorable; @@ -17497,9 +17565,13 @@ export type User = Node & Actor & PackageOwner & ProjectOwner & RepositoryOwner readonly gistComments: GistCommentConnection; /** A list of the Gists the user has created. */ readonly gists: GistConnection; + /** True if this user/organization has a GitHub Sponsors listing. */ + readonly hasSponsorsListing: Scalars['Boolean']; /** The hovercard information for this user in a given context */ readonly hovercard: Hovercard; readonly id: Scalars['ID']; + /** The interaction ability settings for this user. */ + readonly interactionAbility?: Maybe; /** Whether or not this user is a participant in the GitHub Security Bug Bounty. */ readonly isBountyHunter: Scalars['Boolean']; /** Whether or not this user is a participant in the GitHub Campus Experts Program. */ @@ -17512,6 +17584,8 @@ export type User = Node & Actor & PackageOwner & ProjectOwner & RepositoryOwner readonly isHireable: Scalars['Boolean']; /** Whether or not this user is a site administrator. */ readonly isSiteAdmin: Scalars['Boolean']; + /** True if the viewer is sponsored by this user/organization. */ + readonly isSponsoringViewer: Scalars['Boolean']; /** Whether or not this user is the viewing user. */ readonly isViewer: Scalars['Boolean']; /** A list of issue comments made by this user. */ @@ -17562,7 +17636,7 @@ export type User = Node & Actor & PackageOwner & ProjectOwner & RepositoryOwner readonly resourcePath: Scalars['URI']; /** Replies this user has saved */ readonly savedReplies?: Maybe; - /** The GitHub Sponsors listing for this user. */ + /** The GitHub Sponsors listing for this user or organization. */ readonly sponsorsListing?: Maybe; /** This object's sponsorships as the maintainer. */ readonly sponsorshipsAsMaintainer: SponsorshipConnection; @@ -17586,8 +17660,12 @@ export type User = Node & Actor & PackageOwner & ProjectOwner & RepositoryOwner readonly viewerCanCreateProjects: Scalars['Boolean']; /** Whether or not the viewer is able to follow the user. */ readonly viewerCanFollow: Scalars['Boolean']; + /** Whether or not the viewer is able to sponsor this user/organization. */ + readonly viewerCanSponsor: Scalars['Boolean']; /** Whether or not this user is followed by the viewer. */ readonly viewerIsFollowing: Scalars['Boolean']; + /** True if the viewer is sponsoring this user/organization. */ + readonly viewerIsSponsoring: Scalars['Boolean']; /** A list of repositories the given user is watching. */ readonly watching: RepositoryConnection; /** A URL pointing to the user's public website/blog. */ @@ -17676,6 +17754,7 @@ export type UserHovercardArgs = { /** A user is an individual's account on GitHub that owns repositories and can make new content. */ export type UserIssueCommentsArgs = { + orderBy?: Maybe; after?: Maybe; before?: Maybe; first?: Maybe; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 6792721..1f2f323 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -5,6 +5,7 @@ import { Auth } from "features"; const HomePage = lazy(() => import("./home")); const RepositoryPage = lazy(() => import("./repository")); const UserPage = lazy(() => import("./user")); +const SearchPage = lazy(() => import("./search")); /** * Роутинг приложения @@ -12,6 +13,7 @@ const UserPage = lazy(() => import("./user")); const Routing = () => ( + diff --git a/src/pages/search/index.scss b/src/pages/search/index.scss new file mode 100644 index 0000000..d013dea --- /dev/null +++ b/src/pages/search/index.scss @@ -0,0 +1,20 @@ +.page-search { + .result__item { + user-select: none; + border-radius: 10px; + box-shadow: 0 5px 0 var(--clr-gray--900); + transition: var(--transition); + + &:hover { + box-shadow: 0 5px 0 var(--clr-primary--hover); + } + + &:active { + box-shadow: 0 5px 0 var(--clr-primary--active); + } + } + + .filters__item { + text-align: left; + } +} diff --git a/src/pages/search/index.tsx b/src/pages/search/index.tsx new file mode 100644 index 0000000..5707ff3 --- /dev/null +++ b/src/pages/search/index.tsx @@ -0,0 +1,105 @@ +import React from "react"; +import { Row, Col, Skeleton, Empty } from "antd"; +import { useQueryParam, StringParam } from "use-query-params"; +import { Repo, User, Tabs } from "shared/components"; +import { SearchType } from "models"; +import { useSearchQuery, RepoFieldsFragment, UserFieldsFragment } from "./queries.gen"; +import "./index.scss"; + +// !!! FIXME: decompose + +const typesMap: { + [key: string]: SearchType; +} = { + repositories: SearchType.Repository, + users: SearchType.User, +}; + +/** + * @page Search + * !!! TODO: split by features!!! + */ +const SearchPage = () => { + const [searchQuery] = useQueryParam("q", StringParam); + const [searchType, setSearchType] = useQueryParam("type", StringParam); + const searchTypeEnum = typesMap[searchType || "repositories"]; + const { data, loading } = useSearchQuery({ + variables: { + type: searchTypeEnum, + query: searchQuery || "", + }, + }); + + const isEmpty = !loading && (!data || data.search.edges?.length === 0); + + return ( + + +

+ Results by {searchQuery} search: +

+
+ {loading && ( + <> + + + + + )} + {/* FIXME: as wrapper? */} + {/* FIXME: Пока что фильтруем Организации, т.к. под них нужна отдельная страница и логика */} + {data?.search.edges + // @ts-ignore FIXME: specify types + ?.filter((edge) => edge?.node?.__typename !== "Organization") + .map((edge) => { + // !!! FIXME: specify types + // FIXME: simplify + if (searchTypeEnum === SearchType.Repository) { + const data = edge?.node as RepoFieldsFragment; + return ( + + + + ); + } + if (searchTypeEnum === SearchType.User) { + const data = edge?.node as UserFieldsFragment; + return ( + + + + ); + } + return null; + })} + {isEmpty && } +
+ + + {/* FIXME: resolve on tabs level */} + + {/* FIXME: resolve on tabs level */} + {/* FIXME: simplify */} + setSearchType("repositories")} + /> + setSearchType("users")} + /> + + +
+ ); +}; + +const ResultItem = ({ children }: PropsWithChildren) => ( +
{children}
+); + +export default SearchPage; diff --git a/src/pages/search/queries.gen.ts b/src/pages/search/queries.gen.ts new file mode 100644 index 0000000..be81711 --- /dev/null +++ b/src/pages/search/queries.gen.ts @@ -0,0 +1,82 @@ +/** @generated THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. */ +import * as Types from '../../models.gen'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +export type RepoFieldsFragment = { readonly id: string, readonly name: string, readonly url: any, readonly updatedAt: any, readonly viewerHasStarred: boolean, readonly owner: { readonly login: string } | { readonly login: string }, readonly primaryLanguage?: Types.Maybe<{ readonly name: string, readonly color?: Types.Maybe }> }; + +export type UserFieldsFragment = { readonly id: string, readonly login: string, readonly bio?: Types.Maybe, readonly avatarUrl: any, readonly viewerIsFollowing: boolean }; + +export type SearchQueryVariables = Types.Exact<{ + query: Types.Scalars['String']; + type: Types.SearchType; +}>; + + +export type SearchQuery = { readonly search: { readonly edges?: Types.Maybe }>>> } }; + +export const RepoFieldsFragmentDoc = gql` + fragment RepoFields on Repository { + id + name + url + owner { + login + } + updatedAt + primaryLanguage { + name + color + } + viewerHasStarred +} + `; +export const UserFieldsFragmentDoc = gql` + fragment UserFields on User { + id + login + bio + avatarUrl + viewerIsFollowing +} + `; +export const SearchDocument = gql` + query Search($query: String!, $type: SearchType!) { + search(query: $query, type: $type, first: 50) { + edges { + node { + ...RepoFields + ...UserFields + } + } + } +} + ${RepoFieldsFragmentDoc} +${UserFieldsFragmentDoc}`; + +/** + * __useSearchQuery__ + * + * To run a query within a React component, call `useSearchQuery` and pass it any options that fit your needs. + * When your component renders, `useSearchQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useSearchQuery({ + * variables: { + * query: // value for 'query' + * type: // value for 'type' + * }, + * }); + */ +export function useSearchQuery(baseOptions?: Apollo.QueryHookOptions) { + return Apollo.useQuery(SearchDocument, baseOptions); + } +export function useSearchLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + return Apollo.useLazyQuery(SearchDocument, baseOptions); + } +export type SearchQueryHookResult = ReturnType; +export type SearchLazyQueryHookResult = ReturnType; +export type SearchQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/src/pages/search/queries.gql b/src/pages/search/queries.gql new file mode 100644 index 0000000..71473fe --- /dev/null +++ b/src/pages/search/queries.gql @@ -0,0 +1,38 @@ +enum SearchType { + REPOSITORY + USER +} + +fragment RepoFields on Repository { + id + name + url + owner { + login + } + updatedAt + primaryLanguage { + name + color + } + viewerHasStarred +} + +fragment UserFields on User { + id + login + bio + avatarUrl + viewerIsFollowing +} + +query Search($query: String!, $type: SearchType!) { + search(query: $query, type: $type, first: 50) { + edges { + node { + ...RepoFields + ...UserFields + } + } + } +} diff --git a/src/shared/components/.gitkeep b/src/shared/components/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/shared/components/index.ts b/src/shared/components/index.ts new file mode 100644 index 0000000..48814d9 --- /dev/null +++ b/src/shared/components/index.ts @@ -0,0 +1,3 @@ +export { default as Repo } from "./repo"; +export { default as User } from "./user"; +export { default as Tabs } from "./tabs"; diff --git a/src/features/repo-list/repo-item/fav-btn/heart_active.svg b/src/shared/components/repo/fav-btn/heart_active.svg similarity index 100% rename from src/features/repo-list/repo-item/fav-btn/heart_active.svg rename to src/shared/components/repo/fav-btn/heart_active.svg diff --git a/src/features/repo-list/repo-item/fav-btn/heart_default.svg b/src/shared/components/repo/fav-btn/heart_default.svg similarity index 100% rename from src/features/repo-list/repo-item/fav-btn/heart_default.svg rename to src/shared/components/repo/fav-btn/heart_default.svg diff --git a/src/features/repo-list/repo-item/fav-btn/index.tsx b/src/shared/components/repo/fav-btn/index.tsx similarity index 100% rename from src/features/repo-list/repo-item/fav-btn/index.tsx rename to src/shared/components/repo/fav-btn/index.tsx diff --git a/src/features/repo-list/repo-item/index.scss b/src/shared/components/repo/index.scss similarity index 64% rename from src/features/repo-list/repo-item/index.scss rename to src/shared/components/repo/index.scss index 7daff1c..f6e6b6e 100644 --- a/src/features/repo-list/repo-item/index.scss +++ b/src/shared/components/repo/index.scss @@ -1,10 +1,13 @@ -.repo-item { +.repo { display: inline-flex; justify-content: space-between; width: 100%; height: 140px; padding: 22px; margin-top: 22px; + // FIXME: define on app level + font-family: var(--ff-secondary); + font-size: 14px; background-color: var(--clr-gray--100); border-radius: 10px; @@ -13,16 +16,10 @@ align-content: space-between; a { - font-family: var(--ff-secondary); - font-size: 20px; - font-weight: var(--fw--medium); color: var(--clr-text); } span { - font-family: var(--ff-secondary); - font-size: 14px; - font-weight: var(--fw--regular); line-height: 24px; } } diff --git a/src/shared/components/repo/index.tsx b/src/shared/components/repo/index.tsx new file mode 100644 index 0000000..74be7e2 --- /dev/null +++ b/src/shared/components/repo/index.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import dayjs from "dayjs"; +import { Repository, Language } from "models"; +// FIXME: replace to ant.design icons +import FavBtn from "./fav-btn"; +import "./index.scss"; + +// !!! FIXME: specify types +type Props = any; + +// FIXME: refactor +const Repo = (props: Props) => { + const { name, primaryLanguage, updatedAt, url, viewerHasStarred, owner } = props as Partial< + Repository + >; + + return ( +
+
+ {/* FIXME: hardcoded, replace to generation by {username}/{reponame} */} + + {owner?.login && `${owner.login}/`} + {name} + +
+ + Updated on {dayjs(updatedAt).format("D MMM YYYY")} +
+
+ {viewerHasStarred !== undefined && } +
+ ); +}; + +// FIXME: specify types +// FIXME: move to shared? (ждем пока появится еще хотя бы 1 место использования) +const Lang = ({ color, name }: Partial) => { + if (!color || !name) return null; + return ( +
+ + {name} +
+ ); +}; + +export default Repo; diff --git a/src/shared/components/tabs/index.tsx b/src/shared/components/tabs/index.tsx new file mode 100644 index 0000000..3d92c95 --- /dev/null +++ b/src/shared/components/tabs/index.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import cn from "classnames"; +import Item from "./item"; + +type Props = PropsWithChildren<{ + className?: string; +}>; +const Tabs = ({ children, className }: Props) => { + return
{children}
; +}; + +Tabs.Item = Item; +export default Tabs; diff --git a/src/shared/components/tabs/item/index.scss b/src/shared/components/tabs/item/index.scss new file mode 100644 index 0000000..a605d05 --- /dev/null +++ b/src/shared/components/tabs/item/index.scss @@ -0,0 +1,20 @@ +.tab { + padding: 13px 21px; + font-family: var(--ff-primary); + font-size: 19px; + line-height: 33px; + color: #000000; + background: linear-gradient(270deg, rgba(238, 238, 238, 0.24) 35.11%, var(--clr-gray--100) 100%); + border: none; + border-radius: 5px; + outline: none; + transition: var(--transition); + + &:hover { + cursor: pointer; + } + + &.active { + background: linear-gradient(270deg, rgba(51, 153, 255, 0) 8.76%, rgba(51, 153, 255, 0.33) 100%); + } +} diff --git a/src/shared/components/tabs/item/index.tsx b/src/shared/components/tabs/item/index.tsx new file mode 100644 index 0000000..b7b3914 --- /dev/null +++ b/src/shared/components/tabs/item/index.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import cn from "classnames"; +import "./index.scss"; + +type Props = { + name: string; + className?: string; + active?: boolean; + onClick?: Callback; +}; + +const Tab = ({ name, className, active, onClick }: Props) => { + return ( + + ); +}; + +export default Tab; diff --git a/src/shared/components/user/index.scss b/src/shared/components/user/index.scss new file mode 100644 index 0000000..6c99a69 --- /dev/null +++ b/src/shared/components/user/index.scss @@ -0,0 +1,7 @@ +.user { + // FIXME: define on app level + font-family: var(--ff-secondary); + font-size: 14px; + background-color: var(--clr-gray--100); + border-radius: 10px; +} diff --git a/src/shared/components/user/index.tsx b/src/shared/components/user/index.tsx new file mode 100644 index 0000000..b5e4321 --- /dev/null +++ b/src/shared/components/user/index.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { Button } from "antd"; +import "./index.scss"; + +// !!! FIXME: specify types +const User = (props: any) => { + const { avatarUrl, login, viewerIsFollowing, bio } = props as Partial; + return ( +
+
+ avatar +
+
+ + {login} + + {bio} +
+
+ +
+
+ ); +}; + +export default User;