Merge commit 'refs/pull/151/head' of https://github.com/peakshift/makers.bolt.fun into image-upload-management

This commit is contained in:
MTG2000
2022-09-13 10:04:26 +03:00
77 changed files with 3389 additions and 1232 deletions

View File

@@ -28,6 +28,11 @@ declare global {
} }
export interface NexusGenInputs { export interface NexusGenInputs {
ImageInput: { // input type
id?: string | null; // String
name?: string | null; // String
url: string; // String!
}
MakerRoleInput: { // input type MakerRoleInput: { // input type
id: number; // Int! id: number; // Int!
level: NexusGenEnums['RoleLevelEnum']; // RoleLevelEnum! level: NexusGenEnums['RoleLevelEnum']; // RoleLevelEnum!
@@ -36,7 +41,7 @@ export interface NexusGenInputs {
id: number; // Int! id: number; // Int!
} }
ProfileDetailsInput: { // input type ProfileDetailsInput: { // input type
avatar?: string | null; // String avatar?: NexusGenInputs['ImageInput'] | null; // ImageInput
bio?: string | null; // String bio?: string | null; // String
discord?: string | null; // String discord?: string | null; // String
email?: string | null; // String email?: string | null; // String
@@ -59,7 +64,7 @@ export interface NexusGenInputs {
} }
StoryInputType: { // input type StoryInputType: { // input type
body: string; // String! body: string; // String!
cover_image?: string | null; // String cover_image?: NexusGenInputs['ImageInput'] | null; // ImageInput
id?: number | null; // Int id?: number | null; // Int
is_published?: boolean | null; // Boolean is_published?: boolean | null; // Boolean
tags: string[]; // [String!]! tags: string[]; // [String!]!
@@ -94,7 +99,6 @@ export interface NexusGenScalars {
export interface NexusGenObjects { export interface NexusGenObjects {
Author: { // root type Author: { // root type
avatar: string; // String!
id: number; // Int! id: number; // Int!
join_date: NexusGenScalars['Date']; // Date! join_date: NexusGenScalars['Date']; // Date!
lightning_address?: string | null; // String lightning_address?: string | null; // String
@@ -110,7 +114,6 @@ export interface NexusGenObjects {
applicants_count: number; // Int! applicants_count: number; // Int!
applications: NexusGenRootTypes['BountyApplication'][]; // [BountyApplication!]! applications: NexusGenRootTypes['BountyApplication'][]; // [BountyApplication!]!
body: string; // String! body: string; // String!
cover_image?: string | null; // String
createdAt: NexusGenScalars['Date']; // Date! createdAt: NexusGenScalars['Date']; // Date!
deadline: string; // String! deadline: string; // String!
excerpt: string; // String! excerpt: string; // String!
@@ -128,7 +131,6 @@ export interface NexusGenObjects {
workplan: string; // String! workplan: string; // String!
} }
Category: { // root type Category: { // root type
cover_image?: string | null; // String
icon?: string | null; // String icon?: string | null; // String
id: number; // Int! id: number; // Int!
title: string; // String! title: string; // String!
@@ -153,7 +155,6 @@ export interface NexusGenObjects {
title: string; // String! title: string; // String!
} }
Hackathon: { // root type Hackathon: { // root type
cover_image: string; // String!
description: string; // String! description: string; // String!
end_date: NexusGenScalars['Date']; // Date! end_date: NexusGenScalars['Date']; // Date!
id: number; // Int! id: number; // Int!
@@ -180,7 +181,6 @@ export interface NexusGenObjects {
} }
Mutation: {}; Mutation: {};
MyProfile: { // root type MyProfile: { // root type
avatar: string; // String!
bio?: string | null; // String bio?: string | null; // String
discord?: string | null; // String discord?: string | null; // String
email?: string | null; // String email?: string | null; // String
@@ -212,13 +212,10 @@ export interface NexusGenObjects {
votes_count: number; // Int! votes_count: number; // Int!
} }
Project: { // root type Project: { // root type
cover_image: string; // String!
description: string; // String! description: string; // String!
id: number; // Int! id: number; // Int!
lightning_address?: string | null; // String lightning_address?: string | null; // String
lnurl_callback_url?: string | null; // String lnurl_callback_url?: string | null; // String
screenshots: string[]; // [String!]!
thumbnail_image: string; // String!
title: string; // String! title: string; // String!
votes_count: number; // Int! votes_count: number; // Int!
website: string; // String! website: string; // String!
@@ -236,7 +233,6 @@ export interface NexusGenObjects {
} }
Story: { // root type Story: { // root type
body: string; // String! body: string; // String!
cover_image?: string | null; // String
createdAt: NexusGenScalars['Date']; // Date! createdAt: NexusGenScalars['Date']; // Date!
excerpt: string; // String! excerpt: string; // String!
id: number; // Int! id: number; // Int!
@@ -253,7 +249,6 @@ export interface NexusGenObjects {
title: string; // String! title: string; // String!
} }
Tournament: { // root type Tournament: { // root type
cover_image: string; // String!
description: string; // String! description: string; // String!
end_date: NexusGenScalars['Date']; // Date! end_date: NexusGenScalars['Date']; // Date!
id: number; // Int! id: number; // Int!
@@ -304,7 +299,6 @@ export interface NexusGenObjects {
projects: NexusGenRootTypes['Project'][]; // [Project!]! projects: NexusGenRootTypes['Project'][]; // [Project!]!
} }
User: { // root type User: { // root type
avatar: string; // String!
bio?: string | null; // String bio?: string | null; // String
discord?: string | null; // String discord?: string | null; // String
github?: string | null; // String github?: string | null; // String

View File

@@ -115,6 +115,12 @@ type Hackathon {
website: String! website: String!
} }
input ImageInput {
id: String
name: String
url: String!
}
type LnurlDetails { type LnurlDetails {
commentAllowed: Int commentAllowed: Int
maxSendable: Int maxSendable: Int
@@ -219,7 +225,7 @@ type PostComment {
} }
input ProfileDetailsInput { input ProfileDetailsInput {
avatar: String avatar: ImageInput
bio: String bio: String
discord: String discord: String
email: String email: String
@@ -330,7 +336,7 @@ type Story implements PostBase {
input StoryInputType { input StoryInputType {
body: String! body: String!
cover_image: String cover_image: ImageInput
id: Int id: Int
is_published: Boolean is_published: Boolean
tags: [String!]! tags: [String!]!

View File

@@ -4,7 +4,8 @@ const {
extendType, extendType,
nonNull, nonNull,
} = require('nexus'); } = require('nexus');
const { prisma } = require('../../../prisma') const { prisma } = require('../../../prisma');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const Category = objectType({ const Category = objectType({
@@ -12,7 +13,18 @@ const Category = objectType({
definition(t) { definition(t) {
t.nonNull.int('id'); t.nonNull.int('id');
t.nonNull.string('title'); t.nonNull.string('title');
t.string('cover_image'); t.string('cover_image', {
async resolve(parent) {
if (!parent.cover_image_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.cover_image_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.string('icon'); t.string('icon');

View File

@@ -6,6 +6,7 @@ const {
nonNull, nonNull,
} = require('nexus'); } = require('nexus');
const { prisma } = require('../../../prisma'); const { prisma } = require('../../../prisma');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
@@ -15,7 +16,18 @@ const Hackathon = objectType({
t.nonNull.int('id'); t.nonNull.int('id');
t.nonNull.string('title'); t.nonNull.string('title');
t.nonNull.string('description'); t.nonNull.string('description');
t.nonNull.string('cover_image'); t.nonNull.string('cover_image', {
async resolve(parent) {
if (!parent.cover_image_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.cover_image_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.date('start_date'); t.nonNull.date('start_date');
t.nonNull.date('end_date'); t.nonNull.date('end_date');
t.nonNull.string('location'); t.nonNull.string('location');

View File

@@ -1,4 +1,5 @@
const scalars = require('./_scalars') const scalars = require('./_scalars')
const misc = require('./misc')
const category = require('./category') const category = require('./category')
const project = require('./project') const project = require('./project')
const vote = require('./vote') const vote = require('./vote')
@@ -10,6 +11,7 @@ const donation = require('./donation')
const tag = require('./tag') const tag = require('./tag')
module.exports = { module.exports = {
...misc,
...tag, ...tag,
...scalars, ...scalars,
...category, ...category,

View File

@@ -0,0 +1,19 @@
const { objectType, extendType, inputObjectType } = require("nexus");
const { prisma } = require('../../../prisma');
const ImageInput = inputObjectType({
name: 'ImageInput',
definition(t) {
t.string('id');
t.string('name');
t.nonNull.string('url');
}
});
module.exports = {
// Types
ImageInput,
// Queries
}

View File

@@ -15,6 +15,9 @@ const { prisma } = require('../../../prisma');
const { getUserByPubKey } = require('../../../auth/utils/helperFuncs'); const { getUserByPubKey } = require('../../../auth/utils/helperFuncs');
const { ApolloError } = require('apollo-server-lambda'); const { ApolloError } = require('apollo-server-lambda');
const { marked } = require('marked'); const { marked } = require('marked');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { ImageInput } = require('./misc');
const { deleteImage } = require('../../../services/imageUpload.service');
const POST_TYPE = enumType({ const POST_TYPE = enumType({
@@ -37,7 +40,17 @@ const Author = objectType({
definition(t) { definition(t) {
t.nonNull.int('id'); t.nonNull.int('id');
t.nonNull.string('name'); t.nonNull.string('name');
t.nonNull.string('avatar'); t.nonNull.string('avatar', {
async resolve(parent) {
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.avatar_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.date('join_date'); t.nonNull.date('join_date');
t.string('lightning_address'); t.string('lightning_address');
@@ -71,7 +84,18 @@ const Story = objectType({
t.nonNull.string('type', { t.nonNull.string('type', {
resolve: () => t.typeName resolve: () => t.typeName
}); });
t.string('cover_image'); t.string('cover_image', {
async resolve(parent) {
if (!parent.cover_image_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.cover_image_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.list.nonNull.field('comments', { t.nonNull.list.nonNull.field('comments', {
type: "PostComment", type: "PostComment",
resolve: (parent) => [] resolve: (parent) => []
@@ -111,7 +135,9 @@ const StoryInputType = inputObjectType({
t.int('id'); t.int('id');
t.nonNull.string('title'); t.nonNull.string('title');
t.nonNull.string('body'); t.nonNull.string('body');
t.string('cover_image'); t.field('cover_image', {
type: ImageInput
})
t.nonNull.list.nonNull.string('tags'); t.nonNull.list.nonNull.string('tags');
t.boolean('is_published') t.boolean('is_published')
} }
@@ -137,7 +163,18 @@ const Bounty = objectType({
t.nonNull.string('type', { t.nonNull.string('type', {
resolve: () => 'Bounty' resolve: () => 'Bounty'
}); });
t.string('cover_image'); t.string('cover_image', {
async resolve(parent) {
if (!parent.cover_image_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.cover_image_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.string('deadline'); t.nonNull.string('deadline');
t.nonNull.int('reward_amount'); t.nonNull.int('reward_amount');
t.nonNull.int('applicants_count'); t.nonNull.int('applicants_count');
@@ -342,6 +379,64 @@ const getPostById = extendType({
} }
}) })
const addCoverImage = async (providerImageId) => {
const newCoverImage = await prisma.hostedImage.findFirst({
where: {
provider_image_id: providerImageId
}
})
if (!newCoverImage) throw new ApolloError("New cover image not found")
await prisma.hostedImage.update({
where: {
id: newCoverImage.id
},
data: {
is_used: true
}
})
return newCoverImage
}
const getHostedImageIdsFromBody = async (body, oldBodyImagesIds = null) => {
let bodyImageIds = []
const regex = /(?:!\[(.*?)\]\((.*?)\))/g
let match;
while((match = regex.exec(body))) {
const [,,value] = match
// Useful for old external images in case of duplicates. We need to be sure we are targeting an image from the good story.
const where = oldBodyImagesIds ? {
AND: [
{ url: value },
{ id: { in: oldBodyImagesIds } }
]
} :
{
url: value,
}
const hostedImage = await prisma.hostedImage.findFirst({
where
})
if (hostedImage) {
bodyImageIds.push(hostedImage.id)
await prisma.hostedImage.update({
where: {
id: hostedImage.id
},
data: {
is_used: true
}
})
}
}
return bodyImageIds
}
const createStory = extendType({ const createStory = extendType({
type: 'Mutation', type: 'Mutation',
definition(t) { definition(t) {
@@ -358,20 +453,64 @@ const createStory = extendType({
let was_published = false; let was_published = false;
// TODO: validate post data
let coverImage = null
let bodyImageIds = []
// Edit story
if (id) { if (id) {
const oldPost = await prisma.story.findFirst({ const oldPost = await prisma.story.findFirst({
where: { id }, where: { id },
select: { select: {
user_id: true, user_id: true,
is_published: true is_published: true,
cover_image_id: true,
body_image_ids: true
} }
}) })
was_published = oldPost.is_published; was_published = oldPost.is_published;
if (user.id !== oldPost.user_id) if (user.id !== oldPost.user_id) throw new ApolloError("Not post author")
throw new ApolloError("Not post author")
}
// TODO: validate post data
// Body images
bodyImageIds = await getHostedImageIdsFromBody(body, oldPost.body_image_ids)
// Old cover image is found
if (oldPost.cover_image_id) {
const oldCoverImage = await prisma.hostedImage.findFirst({
where : {
id: oldPost.cover_image_id
}
})
// New cover image
if (cover_image?.id && cover_image.id !== oldCoverImage?.provider_image_id) {
await deleteImage(oldCoverImage.id)
coverImage = await addCoverImage(cover_image.id)
} else {
coverImage = oldCoverImage
}
} else {
// No old image found and new cover image
if (cover_image?.id) {
coverImage = await addCoverImage(cover_image.id)
}
}
// Remove unused body images
const unusedImagesIds = oldPost.body_image_ids.filter(x => !bodyImageIds.includes(x));
unusedImagesIds.map(async i => await deleteImage(i))
} else {
// Body images
bodyImageIds = await getHostedImageIdsFromBody(body)
// New story and new cover image
if (cover_image?.id) {
coverImage = await addCoverImage(cover_image.id)
}
}
// Preprocess & insert // Preprocess & insert
const htmlBody = marked.parse(body); const htmlBody = marked.parse(body);
@@ -383,6 +522,16 @@ const createStory = extendType({
.replace(/"/g, '"') .replace(/"/g, '"')
; ;
const coverImageRel = coverImage ? {
cover_image_rel: {
connect:
{
id: coverImage ? coverImage.id : null
}
}
} : {}
if (id) { if (id) {
await prisma.story.update({ await prisma.story.update({
where: { id }, where: { id },
@@ -398,7 +547,7 @@ const createStory = extendType({
data: { data: {
title, title,
body, body,
cover_image, cover_image: '',
excerpt, excerpt,
is_published: was_published || is_published, is_published: was_published || is_published,
tags: { tags: {
@@ -415,16 +564,17 @@ const createStory = extendType({
} }
}) })
}, },
body_image_ids: bodyImageIds,
...coverImageRel
} }
}) })
} }
return await prisma.story.create({
return prisma.story.create({
data: { data: {
title, title,
body, body,
cover_image, cover_image: '',
excerpt, excerpt,
is_published, is_published,
tags: { tags: {
@@ -445,7 +595,9 @@ const createStory = extendType({
connect: { connect: {
id: user.id, id: user.id,
} }
} },
body_image_ids: bodyImageIds,
...coverImageRel
} }
}) })
} }
@@ -470,17 +622,39 @@ const deleteStory = extendType({
const oldPost = await prisma.story.findFirst({ const oldPost = await prisma.story.findFirst({
where: { id }, where: { id },
select: { select: {
user_id: true user_id: true,
body_image_ids: true,
cover_image_id: true
} }
}) })
if (user.id !== oldPost.user_id) if (user.id !== oldPost.user_id)
throw new ApolloError("Not post author") throw new ApolloError("Not post author")
return prisma.story.delete({ const deletedPost = await prisma.story.delete({
where: { where: {
id id
} }
}) })
const coverImage = await prisma.hostedImage.findMany({
where: {
OR: [
{ id: oldPost.cover_image_id },
{
id: {
in: oldPost.body_image_ids
}
}
]
},
select: {
id: true,
provider_image_id: true
}
})
coverImage.map(async i => await deleteImage(i.id))
return deletedPost
} }
}) })
}, },

View File

@@ -6,6 +6,7 @@ const {
nonNull, nonNull,
} = require('nexus') } = require('nexus')
const { prisma } = require('../../../prisma'); const { prisma } = require('../../../prisma');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { paginationArgs, getLnurlDetails, lightningAddressToLnurl } = require('./helpers'); const { paginationArgs, getLnurlDetails, lightningAddressToLnurl } = require('./helpers');
const { MakerRole } = require('./users'); const { MakerRole } = require('./users');
@@ -17,9 +18,44 @@ const Project = objectType({
t.nonNull.int('id'); t.nonNull.int('id');
t.nonNull.string('title'); t.nonNull.string('title');
t.nonNull.string('description'); t.nonNull.string('description');
t.nonNull.string('cover_image'); t.nonNull.string('cover_image', {
t.nonNull.string('thumbnail_image'); async resolve(parent) {
t.nonNull.list.nonNull.string('screenshots'); if (!parent.cover_image_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.cover_image_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.string('thumbnail_image', {
async resolve(parent) {
if (!parent.thumbnail_image_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.thumbnail_image_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.list.nonNull.string('screenshots', {
async resolve(parent) {
if (!parent.screenshots_ids) return null
const imgObject = await prisma.hostedImage.findMany({
where: {
id: { in: parent.screenshots_ids }
}
});
return imgObject.map(img => {
return resolveImgObjectToUrl(img);
});
}
});
t.nonNull.string('website'); t.nonNull.string('website');
t.string('lightning_address'); t.string('lightning_address');
t.string('lnurl_callback_url'); t.string('lnurl_callback_url');

View File

@@ -3,7 +3,10 @@ const { prisma } = require('../../../prisma');
const { objectType, extendType, intArg, nonNull, inputObjectType, interfaceType, list, enumType } = require("nexus"); const { objectType, extendType, intArg, nonNull, inputObjectType, interfaceType, list, enumType } = require("nexus");
const { getUserByPubKey } = require("../../../auth/utils/helperFuncs"); const { getUserByPubKey } = require("../../../auth/utils/helperFuncs");
const { removeNulls } = require("./helpers"); const { removeNulls } = require("./helpers");
const { ImageInput } = require('./misc');
const { Tournament } = require('./tournament'); const { Tournament } = require('./tournament');
const { resolveImgObjectToUrl } = require('../../../utils/resolveImageUrl');
const { deleteImage } = require('../../../services/imageUpload.service');
@@ -13,7 +16,18 @@ const BaseUser = interfaceType({
definition(t) { definition(t) {
t.nonNull.int('id'); t.nonNull.int('id');
t.nonNull.string('name'); t.nonNull.string('name');
t.nonNull.string('avatar'); t.nonNull.string('avatar', {
async resolve(parent) {
if (!parent.avatar_id) return null
const imgObject = await prisma.hostedImage.findUnique({
where: {
id: parent.avatar_id
}
});
return resolveImgObjectToUrl(imgObject);
}
});
t.nonNull.date('join_date'); t.nonNull.date('join_date');
t.string('role'); t.string('role');
t.string('jobTitle') t.string('jobTitle')
@@ -266,7 +280,9 @@ const ProfileDetailsInput = inputObjectType({
name: 'ProfileDetailsInput', name: 'ProfileDetailsInput',
definition(t) { definition(t) {
t.string('name'); t.string('name');
t.string('avatar'); t.field('avatar', {
type: ImageInput
})
t.string('email') t.string('email')
t.string('jobTitle') t.string('jobTitle')
t.string('lightning_address') t.string('lightning_address')
@@ -294,14 +310,48 @@ const updateProfileDetails = extendType({
throw new Error("You have to login"); throw new Error("You have to login");
// TODO: validate new data // TODO: validate new data
// ----------------
// Check if the user uploaded a new image, and if so,
// remove the old one from the hosting service, then replace it with this one
// ----------------
let avatarId = user.avatar_id;
if (args.data.avatar.id) {
const newAvatarProviderId = args.data.avatar.id;
const newAvatar = await prisma.hostedImage.findFirst({
where: {
provider_image_id: newAvatarProviderId
}
})
if (newAvatar && newAvatar.id !== user.avatar_id) {
avatarId = newAvatar.id;
await prisma.hostedImage.update({
where: {
id: newAvatar.id
},
data: {
is_used: true
}
});
await deleteImage(user.avatar_id)
}
}
// Preprocess & insert // Preprocess & insert
return prisma.user.update({ return prisma.user.update({
where: { where: {
id: user.id, id: user.id,
}, },
data: removeNulls(args.data) data: removeNulls({
...args.data,
avatar_id: avatarId,
//hack to remove avatar from args.data
// can be removed later with a schema data validator
avatar: '',
})
}) })
} }
}) })

View File

@@ -85,11 +85,21 @@ const loginHandler = async (req, res) => {
const nostr_prv_key = generatePrivateKey(); const nostr_prv_key = generatePrivateKey();
const nostr_pub_key = getPublicKey(nostr_prv_key); const nostr_pub_key = getPublicKey(nostr_prv_key);
const avatar = await prisma.hostedImage.create({
data: {
filename: 'avatar.svg',
provider: 'external',
is_used: true,
url: `https://avatars.dicebear.com/api/bottts/${key}.svg`,
provider_image_id: ''
}
})
const createdUser = await prisma.user.create({ const createdUser = await prisma.user.create({
data: { data: {
pubKey: key, pubKey: key,
name: key, name: key,
avatar: `https://avatars.dicebear.com/api/bottts/${key}.svg`, avatar_id: avatar.id,
nostr_prv_key, nostr_prv_key,
nostr_pub_key, nostr_pub_key,
}, },

View File

@@ -5,11 +5,10 @@ const extractKeyFromCookie = require('../../utils/extractKeyFromCookie')
const { getUserByPubKey } = require('../../auth/utils/helperFuncs') const { getUserByPubKey } = require('../../auth/utils/helperFuncs')
const { getDirectUploadUrl } = require('../../services/imageUpload.service') const { getDirectUploadUrl } = require('../../services/imageUpload.service')
const { prisma } = require('../../prisma') const { prisma } = require('../../prisma')
const { getUrlFromProvider } = require('../../utils/resolveImageUrl')
const postUploadImageUrl = async (req, res) => { const postUploadImageUrl = async (req, res) => {
return res.status(404).send("This api is in progress");
const userPubKey = await extractKeyFromCookie(req.headers.cookie ?? req.headers.Cookie) const userPubKey = await extractKeyFromCookie(req.headers.cookie ?? req.headers.Cookie)
const user = await getUserByPubKey(userPubKey) const user = await getUserByPubKey(userPubKey)
@@ -22,11 +21,16 @@ const postUploadImageUrl = async (req, res) => {
try { try {
const uploadUrl = await getDirectUploadUrl() const uploadUrl = await getDirectUploadUrl()
await prisma.hostedImage.create({ const hostedImage = await prisma.hostedImage.create({
data: { id: uploadUrl.id, filename }, data: {
filename,
url: getUrlFromProvider(uploadUrl.provider, uploadUrl.id),
provider_image_id: uploadUrl.id,
provider: uploadUrl.provider
},
}) })
return res.status(200).json(uploadUrl) return res.status(200).json({ id: hostedImage.id, uploadURL: uploadUrl.uploadURL })
} catch (error) { } catch (error) {
res.status(500).send('Unexpected error happened, please try again') res.status(500).send('Unexpected error happened, please try again')
} }

View File

@@ -1,11 +1,21 @@
const { CONSTS } = require('../utils') const { CONSTS } = require('../utils')
const axios = require('axios') const axios = require('axios')
const FormData = require('form-data') const FormData = require('form-data')
const { prisma } = require('../prisma')
const BASE_URL = 'https://api.cloudflare.com/client/v4' const BASE_URL = 'https://api.cloudflare.com/client/v4'
const operationUrls = { const operationUrls = {
'image.uploadUrl': `${BASE_URL}/accounts/${CONSTS.CLOUDFLARE_IMAGE_ACCOUNT_ID}/images/v2/direct_upload`, 'image.uploadUrl': `${BASE_URL}/accounts/${CONSTS.CLOUDFLARE_IMAGE_ACCOUNT_ID}/images/v2/direct_upload`,
'image.delete': `${BASE_URL}/accounts/${CONSTS.CLOUDFLARE_IMAGE_ACCOUNT_ID}/images/v1/`,
}
function getAxiosConfig() {
return {
headers: {
Authorization: `Bearer ${CONSTS.CLOUDFLARE_IMAGE_API_KEY}`,
},
}
} }
async function getDirectUploadUrl() { async function getDirectUploadUrl() {
@@ -27,9 +37,62 @@ async function getDirectUploadUrl() {
throw new Error(result.data, { cause: result.data.errors }) throw new Error(result.data, { cause: result.data.errors })
} }
return result.data.result const data = result.data.result
return { id: data.id, uploadURL: data.uploadURL, provider: 'cloudflare' }
}
async function deleteImageFromProvider(providerImageId) {
try {
const url = operationUrls['image.delete'] + providerImageId
const result = await axios.delete(url, getAxiosConfig())
if (!result.data.success) {
throw new Error(result.data, { cause: result.data.errors })
}
} catch (error) {
throw error
}
}
async function deleteImage(hostedImageId) {
if (!hostedImageId) throw new Error("argument 'hostedImageId' must be provider")
const hostedImage = await prisma.hostedImage.findFirst({
where: {
id: hostedImageId,
},
})
if (!hostedImage) throw new Error(`No HostedImage row found for HostedImage.id=${hostedImageId}`)
if (hostedImage.provider_image_id && hostedImage.provider_image_id === '')
throw new Error(`Field 'provider_image_id' for HostedImage.id=${hostedImageId} must not be empty. Current value '${hostedImage.provider_image_id}'`)
// Set is_used to false in case of deletion fail from the hosting image provider. The scheduled job will try to delete the HostedImage row
await prisma.hostedImage.update({
where: {
id: hostedImage.id,
},
data: {
is_used: false,
},
})
if (hostedImage.provider_image_id && hostedImage.provider_image_id !== '') {
deleteImageFromProvider(hostedImage.provider_image_id)
.then(async () => {
await prisma.hostedImage.delete({
where: {
id: hostedImageId,
},
})
})
.catch((error) => console.error(error))
}
} }
module.exports = { module.exports = {
getDirectUploadUrl, getDirectUploadUrl,
deleteImage,
deleteImageFromProvider,
} }

View File

@@ -3,6 +3,7 @@ const JWT_SECRET = process.env.JWT_SECRET
const LNURL_AUTH_HOST = process.env.LNURL_AUTH_HOST const LNURL_AUTH_HOST = process.env.LNURL_AUTH_HOST
const CLOUDFLARE_IMAGE_ACCOUNT_ID = process.env.CLOUDFLARE_IMAGE_ACCOUNT_ID const CLOUDFLARE_IMAGE_ACCOUNT_ID = process.env.CLOUDFLARE_IMAGE_ACCOUNT_ID
const CLOUDFLARE_IMAGE_API_KEY = process.env.CLOUDFLARE_IMAGE_API_KEY const CLOUDFLARE_IMAGE_API_KEY = process.env.CLOUDFLARE_IMAGE_API_KEY
const CLOUDFLARE_IMAGE_ACCOUNT_HASH = process.env.CLOUDFLARE_IMAGE_ACCOUNT_HASH
const CONSTS = { const CONSTS = {
JWT_SECRET, JWT_SECRET,
@@ -10,6 +11,7 @@ const CONSTS = {
LNURL_AUTH_HOST, LNURL_AUTH_HOST,
CLOUDFLARE_IMAGE_ACCOUNT_ID, CLOUDFLARE_IMAGE_ACCOUNT_ID,
CLOUDFLARE_IMAGE_API_KEY, CLOUDFLARE_IMAGE_API_KEY,
CLOUDFLARE_IMAGE_ACCOUNT_HASH
} }
module.exports = CONSTS module.exports = CONSTS

View File

@@ -0,0 +1,45 @@
const { CLOUDFLARE_IMAGE_ACCOUNT_HASH } = require('./consts')
const PROVIDERS = [
{
name: 'cloudflare',
prefixUrl: `https://imagedelivery.net/${CLOUDFLARE_IMAGE_ACCOUNT_HASH}/`,
variants: [
{
default: true,
name: 'public',
},
],
},
]
/**
* resolveImgObjectToUrl
* @param {object} imgObject
* @param {string} variant - List to be defined. DEFAULT TO 'public'
* @returns {string} image url
*/
function resolveImgObjectToUrl(imgObject, variant = null) {
if (!imgObject) throw new Error('Image not found')
if (imgObject.provider === 'external') {
return imgObject.url
}
return getUrlFromProvider(imgObject.provider, imgObject.provider_image_id, variant)
}
function getUrlFromProvider(provider, providerImageId, variant = null) {
const p = PROVIDERS.find((p) => p.name === provider)
if (p) {
if (p && p.name === 'cloudflare') {
const variantName = variant ?? p.variants.find((v) => v.default).name
return p.prefixUrl + providerImageId + '/' + variantName
}
}
throw new Error('Hosting images provider not supported')
}
module.exports = { resolveImgObjectToUrl, getUrlFromProvider }

659
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,11 @@
"@reduxjs/toolkit": "^1.8.1", "@reduxjs/toolkit": "^1.8.1",
"@remirror/pm": "^1.0.16", "@remirror/pm": "^1.0.16",
"@remirror/react": "^1.0.34", "@remirror/react": "^1.0.34",
"@rpldy/mock-sender": "^1.0.1",
"@rpldy/upload-button": "^1.0.1",
"@rpldy/upload-drop-zone": "^1.0.1",
"@rpldy/upload-preview": "^1.0.1",
"@rpldy/uploady": "^1.0.1",
"@shopify/react-web-worker": "^5.0.1", "@shopify/react-web-worker": "^5.0.1",
"@szhsin/react-menu": "^3.0.2", "@szhsin/react-menu": "^3.0.2",
"@testing-library/jest-dom": "^5.16.4", "@testing-library/jest-dom": "^5.16.4",

View File

@@ -0,0 +1,29 @@
/*
Warnings:
- The primary key for the `HostedImage` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The `id` column on the `HostedImage` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- Added the required column `provider` to the `HostedImage` table without a default value. This is not possible if the table is not empty.
- Added the required column `provider_image_id` to the `HostedImage` table without a default value. This is not possible if the table is not empty.
- Added the required column `url` to the `HostedImage` table without a default value. This is not possible if the table is not empty.
*/
-- START Custom SQL
-- Because of the breaking changes, we have to apply some custom SQL.
-- By chance, the previous HostedImage migration is not used it production, so we can remove all data from this table
DELETE FROM "HostedImage";
-- END Custom SQL
-- AlterTable
ALTER TABLE "HostedImage" DROP CONSTRAINT "HostedImage_pkey",
ADD COLUMN "provider" TEXT NOT NULL,
ADD COLUMN "provider_image_id" TEXT NOT NULL,
ADD COLUMN "url" TEXT NOT NULL,
DROP COLUMN "id",
ADD COLUMN "id" SERIAL NOT NULL,
ADD CONSTRAINT "HostedImage_pkey" PRIMARY KEY ("id");
-- AlterTable
ALTER TABLE "UserKey" ALTER COLUMN "name" SET DEFAULT E'My new wallet key';

View File

@@ -0,0 +1,74 @@
/*
Warnings:
- A unique constraint covering the columns `[image_id]` on the table `Award` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[cover_image_id]` on the table `Category` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[cover_image_id]` on the table `Hackathon` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[thumbnail_image_id]` on the table `Project` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[cover_image_id]` on the table `Project` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[cover_image_id]` on the table `Story` will be added. If there are existing duplicate values, this will fail.
- A unique constraint covering the columns `[avatar_id]` on the table `User` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Award" ADD COLUMN "image_id" INTEGER;
-- AlterTable
ALTER TABLE "Category" ADD COLUMN "cover_image_id" INTEGER;
-- AlterTable
ALTER TABLE "Hackathon" ADD COLUMN "cover_image_id" INTEGER;
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "cover_image_id" INTEGER,
ADD COLUMN "screenshots_ids" INTEGER[],
ADD COLUMN "thumbnail_image_id" INTEGER;
-- AlterTable
ALTER TABLE "Story" ADD COLUMN "body_image_ids" INTEGER[],
ADD COLUMN "cover_image_id" INTEGER;
-- AlterTable
ALTER TABLE "User" ADD COLUMN "avatar_id" INTEGER;
-- CreateIndex
CREATE UNIQUE INDEX "Award_image_id_key" ON "Award"("image_id");
-- CreateIndex
CREATE UNIQUE INDEX "Category_cover_image_id_key" ON "Category"("cover_image_id");
-- CreateIndex
CREATE UNIQUE INDEX "Hackathon_cover_image_id_key" ON "Hackathon"("cover_image_id");
-- CreateIndex
CREATE UNIQUE INDEX "Project_thumbnail_image_id_key" ON "Project"("thumbnail_image_id");
-- CreateIndex
CREATE UNIQUE INDEX "Project_cover_image_id_key" ON "Project"("cover_image_id");
-- CreateIndex
CREATE UNIQUE INDEX "Story_cover_image_id_key" ON "Story"("cover_image_id");
-- CreateIndex
CREATE UNIQUE INDEX "User_avatar_id_key" ON "User"("avatar_id");
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_avatar_id_fkey" FOREIGN KEY ("avatar_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Category" ADD CONSTRAINT "Category_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Project" ADD CONSTRAINT "Project_thumbnail_image_id_fkey" FOREIGN KEY ("thumbnail_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Project" ADD CONSTRAINT "Project_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Award" ADD CONSTRAINT "Award_image_id_fkey" FOREIGN KEY ("image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Story" ADD CONSTRAINT "Story_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Hackathon" ADD CONSTRAINT "Hackathon_cover_image_id_fkey" FOREIGN KEY ("cover_image_id") REFERENCES "HostedImage"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -44,6 +44,8 @@ model User {
pubKey String? @unique pubKey String? @unique
name String? name String?
avatar String? avatar String?
avatar_id Int? @unique
avatar_rel HostedImage? @relation("User_Avatar", fields: [avatar_id], references: [id])
role String @default("user") role String @default("user")
email String? email String?
@@ -115,6 +117,8 @@ model Category {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
title String title String
cover_image String? cover_image String?
cover_image_id Int? @unique
cover_image_rel HostedImage? @relation("Category_CoverImage", fields: [cover_image_id], references: [id])
icon String? icon String?
project Project[] project Project[]
@@ -125,9 +129,14 @@ model Project {
title String title String
description String description String
screenshots String[] screenshots String[]
screenshots_ids Int[]
website String website String
thumbnail_image String? thumbnail_image String?
thumbnail_image_id Int? @unique
thumbnail_image_rel HostedImage? @relation("Project_Thumbnail", fields: [thumbnail_image_id], references: [id])
cover_image String? cover_image String?
cover_image_id Int? @unique
cover_image_rel HostedImage? @relation("Project_CoverImage", fields: [cover_image_id], references: [id])
lightning_address String? lightning_address String?
lnurl_callback_url String? lnurl_callback_url String?
@@ -158,6 +167,8 @@ model Award {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
title String title String
image String image String
image_id Int? @unique
image_rel HostedImage? @relation("Award_Image", fields: [image_id], references: [id])
url String url String
project Project @relation(fields: [project_id], references: [id]) project Project @relation(fields: [project_id], references: [id])
@@ -174,8 +185,11 @@ model Story {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
body String body String
body_image_ids Int[]
excerpt String excerpt String
cover_image String? cover_image String?
cover_image_id Int? @unique
cover_image_rel HostedImage? @relation("Story_CoverImage", fields: [cover_image_id], references: [id])
votes_count Int @default(0) votes_count Int @default(0)
is_published Boolean @default(true) is_published Boolean @default(true)
@@ -235,6 +249,8 @@ model Hackathon {
start_date DateTime @db.Date start_date DateTime @db.Date
end_date DateTime @db.Date end_date DateTime @db.Date
cover_image String cover_image String
cover_image_id Int? @unique
cover_image_rel HostedImage? @relation("Hackathon_CoverImage", fields: [cover_image_id], references: [id])
description String description String
location String location String
website String website String
@@ -272,10 +288,21 @@ model GeneratedK1 {
// Hosted Image // Hosted Image
// ----------------- // -----------------
model HostedImage { model HostedImage {
id String @id id Int @id @default(autoincrement())
filename String filename String
provider_image_id String
provider String
url String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
is_used Boolean @default(false) is_used Boolean @default(false)
ProjectThumbnail Project? @relation("Project_Thumbnail")
ProjectCoverImage Project? @relation("Project_CoverImage")
CategoryCoverImage Category? @relation("Category_CoverImage")
AwardImage Award? @relation("Award_Image")
HackathonCoverImage Hackathon? @relation("Hackathon_CoverImage")
StoryCoverImage Story? @relation("Story_CoverImage")
User User? @relation("User_Avatar")
} }
// ----------------- // -----------------

View File

@@ -67,6 +67,190 @@ async function main() {
await createTournament(); await createTournament();
// await migrateOldImages();
}
async function migrateOldImages() {
console.log('Migrating old images data to HostedImage');
// Can't use prisma method createMany() for columns like Project.screenshots, because this method doesn't return created IDs.
/**
* Project
**/
const projects = await prisma.project.findMany({
select: {
id: true,
screenshots: true,
cover_image: true,
thumbnail_image: true
}
})
for (const project of projects) {
/**
* Project.screenshots to Project.screenshots_ids
**/
let projectScreenshotIds = [];
for (const screenshot of project.screenshots) {
let hostedImageId = await _insertInHostedImage(screenshot)
projectScreenshotIds.push(hostedImageId);
}
if (projectScreenshotIds.length > 0) {
await _updateObjectWithHostedImageId(prisma.project, project.id, {
screenshots_ids: projectScreenshotIds,
})
}
/**
* Project.cover_image to Project.cover_image_id
**/
if (project.cover_image) {
let hostedImageId = await _insertInHostedImage(project.cover_image)
await _updateObjectWithHostedImageId(prisma.project, project.id, {
cover_image_id: hostedImageId,
})
}
/**
* Project.thumbnail_image to Project.thumbnail_image_id
**/
if (project.cover_image) {
let hostedImageId = await _insertInHostedImage(project.thumbnail_image)
await _updateObjectWithHostedImageId(prisma.project, project.id, {
thumbnail_image_id: hostedImageId,
})
}
}
/**
* Category
**/
const categories = await prisma.category.findMany({
select: {
id: true,
cover_image: true,
}
})
for (const category of categories) {
if (category.cover_image) {
let hostedImageId = await _insertInHostedImage(category.cover_image)
await _updateObjectWithHostedImageId(prisma.category, category.id, {
cover_image_id: hostedImageId,
})
}
}
/**
* Award
**/
const awards = await prisma.award.findMany({
select: {
id: true,
image: true,
}
})
for (const award of awards) {
if (award.image) {
let hostedImageId = await _insertInHostedImage(award.image)
await _updateObjectWithHostedImageId(prisma.award, award.id, {
image_id: hostedImageId,
})
}
}
/**
* Hackaton
**/
const hackatons = await prisma.hackathon.findMany({
select: {
id: true,
cover_image: true,
}
})
for (const hackaton of hackatons) {
if (hackaton.cover_image) {
let hostedImageId = await _insertInHostedImage(hackaton.cover_image)
await _updateObjectWithHostedImageId(prisma.hackathon, hackaton.id, {
cover_image_id: hostedImageId,
})
}
}
/**
* Story
**/
const stories = await prisma.story.findMany({
select: {
id: true,
cover_image: true,
body: true,
}
})
for (const story of stories) {
/**
* Story.body to Story.body_image_ids
**/
let bodyImageIds = [];
const regex = /(?:!\[(.*?)\]\((.*?)\))/g
let match;
while ((match = regex.exec(story.body))) {
const [, , value] = match
let hostedImageId = await _insertInHostedImage(value)
bodyImageIds.push(hostedImageId)
}
if (bodyImageIds.length > 0) {
await _updateObjectWithHostedImageId(prisma.story, story.id, {
body_image_ids: bodyImageIds,
})
}
/**
* Story.cover_image to Story.cover_image_id
**/
if (story.cover_image) {
let hostedImageId = await _insertInHostedImage(story.cover_image)
await _updateObjectWithHostedImageId(prisma.story, story.id, {
cover_image_id: hostedImageId,
})
}
}
/**
* User
**/
const users = await prisma.user.findMany({
select: {
id: true,
avatar: true,
}
})
for (const user of users) {
if (user.avatar) {
let hostedImageId = await _insertInHostedImage(user.avatar)
await _updateObjectWithHostedImageId(prisma.user, user.id, {
avatar_id: hostedImageId,
})
}
}
}
async function _insertInHostedImage(url) {
const newHostedImage = await prisma.hostedImage.create({
data: {
filename: "default.png",
provider: "external",
provider_image_id: "",
url,
is_used: true
}
});
return newHostedImage.id;
}
async function _updateObjectWithHostedImageId(prismaObject, objectId, data) {
await prismaObject.update({
where: { id: objectId },
data,
});
} }
async function createCategories() { async function createCategories() {

View File

@@ -2,7 +2,7 @@
/* tslint:disable */ /* tslint:disable */
/** /**
* Mock Service Worker (0.39.1). * Mock Service Worker (0.39.2).
* @see https://github.com/mswjs/msw * @see https://github.com/mswjs/msw
* - Please do NOT modify this file. * - Please do NOT modify this file.
* - Please do NOT serve this file on production. * - Please do NOT serve this file on production.

View File

@@ -76,7 +76,6 @@ function App() {
}, []); }, []);
return <div id="app" className='w-full'> return <div id="app" className='w-full'>
<Helmet> <Helmet>
<title >makers.bolt.fun</title> <title >makers.bolt.fun</title>

View File

@@ -1,66 +0,0 @@
import { useToggle } from "@react-hookz/web";
import React from "react";
import { FileDrop } from "react-file-drop";
export default function DropInput({
value: files,
onChange,
emptyContent,
draggingContent,
hasFilesContent,
height,
multiple = false,
allowedType = "*",
classes = {
base: "",
idle: "",
dragging: "",
},
}) {
const [isDragging, toggleDrag] = useToggle(false);
const fileInputRef = React.useRef(null);
const onAddFiles = (_files) => {
onChange(_files);
// do something with your files...
};
const uploadClick = () => {
fileInputRef.current.click();
};
const status = isDragging ? "dragging" : files ? "has-files" : "empty";
return (
<div
style={{
height: height + "px",
}}
>
<FileDrop
onDrop={(files) => onAddFiles(files)}
onTargetClick={uploadClick}
onFrameDragEnter={() => toggleDrag(true)}
onFrameDragLeave={() => toggleDrag(false)}
onFrameDrop={() => toggleDrag(false)}
className={`h-full cursor-pointer`}
targetClassName={`h-full ${classes.base} ${
status === "empty" && classes.idle
}`}
draggingOverFrameClassName={`${classes.dragging}`}
>
{status === "dragging" && draggingContent}
{status === "empty" && emptyContent}
{status === "has-files" && hasFilesContent}
</FileDrop>
<input
onChange={(e) => onAddFiles(e.target.files)}
ref={fileInputRef}
type="file"
className="hidden"
multiple={multiple}
accept={allowedType}
/>
</div>
);
}

View File

@@ -1,31 +0,0 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { BsImages } from 'react-icons/bs';
import Button from 'src/Components/Button/Button';
import FilesInput from './FilesInput';
import FileDropInput from './FilesDropInput';
export default {
title: 'Shared/Inputs/Files Input',
component: FilesInput,
} as ComponentMeta<typeof FilesInput>;
const Template: ComponentStory<typeof FilesInput> = (args) => <FilesInput {...args} />
export const DefaultButton = Template.bind({});
DefaultButton.args = {
}
export const CustomizedButton = Template.bind({});
CustomizedButton.args = {
multiple: true,
uploadBtn: <Button color='primary'><span className="align-middle">Drop Images</span> <BsImages className='ml-12 scale-125' /></Button>
}
const DropTemplate: ComponentStory<typeof FileDropInput> = (args) => <div className="max-w-[500px]"><FileDropInput {...args as any} /></div>
export const DropZoneInput = DropTemplate.bind({});
DropZoneInput.args = {
onChange: console.log,
}

View File

@@ -1,75 +0,0 @@
import { useMemo } from "react";
import { MdClose } from "react-icons/md";
import IconButton from "src/Components/IconButton/IconButton";
interface Props {
file: File | string,
onRemove?: () => void
}
function getFileType(file: File | string) {
if (typeof file === 'string') {
if (/^http[^?]*.(jpg|jpeg|gif|png|tiff|bmp)(\?(.*))?$/gmi.test(file))
return 'image'
if (/\.(pdf|doc|docx)$/.test(file))
return 'document';
return 'unknown'
}
else {
if (file['type'].split('/')[0] === 'image')
return 'image'
return 'unknown'
}
}
type ThumbnailFile = {
name: string;
src: string;
type: ReturnType<typeof getFileType>
}
function processFile(file: Props['file']): ThumbnailFile {
const fileType = getFileType(file);
if (typeof file === 'string') return { name: file, src: file, type: fileType };
return {
name: file.name,
src: URL.createObjectURL(file),
type: fileType
};
}
export default function FileThumbnail({ file: f, onRemove }: Props) {
const file = useMemo(() => processFile(f), [f])
return (
<div className="bg-gray-100 rounded-8 p-12 shrink-0 flex gap-4 overflow-hidden">
<div className="w-[100px]">
<p className="text-body6 overflow-hidden overflow-ellipsis whitespace-nowrap">
{file.name}
</p>
<a
href={file.src}
target='_blank'
rel="noreferrer"
>
{
file.type === 'image' && <img src={file.src} alt={file.name} className="p-4 w-3/4 mx-auto max-h-full object-contain" />
}
</a>
</div>
<div className="w-32 shrink-0 self-start" >
<IconButton size="sm" className="hover:bg-gray-500" onClick={onRemove}>
<MdClose />
</IconButton>
</div>
</div>
)
}

View File

@@ -1,88 +0,0 @@
import { FaImage } from "react-icons/fa";
import { UnionToObjectKeys } from "src/utils/types/utils";
import DropInput from "./DropInput";
type Props = {
height?: number
multiple?: boolean;
value?: File[] | string[] | string;
max?: number;
onBlur?: () => void;
onChange?: (files: (File | string)[] | null) => void
uploadBtn?: JSX.Element
uploadText?: string;
allowedType?: 'images';
classes?: Partial<{
base: string,
idle: string,
dragging: string,
hasFiles: string
}>
}
const fileAccept: UnionToObjectKeys<Props, 'allowedType'> = {
images: ".png, .jpg, .jpeg"
} as const;
const fileUrlToObject = async (url: string, fileName: string = 'filename') => {
const res = await fetch(url);
const contentType = res.headers.get('content-type') as string;
const blob = await res.blob()
const file = new File([blob], fileName, { contentType } as any)
return file
}
export default function FilesInput({
height = 200,
multiple,
value,
max = 3,
onBlur,
onChange,
allowedType = 'images',
classes,
...props
}: Props) {
const baseClasses = classes?.base ?? 'p-32 rounded-8 text-center flex flex-col justify-center items-center'
const idleClasses = classes?.idle ?? 'bg-primary-50 hover:bg-primary-25 border border-dashed border-primary-500 text-gray-800'
const draggingClasses = classes?.dragging ?? 'bg-primary-500 text-white'
return (
<DropInput
height={height}
emptyContent={defaultEmptyContent}
draggingContent={defaultDraggingContent}
hasFilesContent={defaultHasFilesContent}
value={value}
onChange={onChange}
multiple={multiple}
allowedType={fileAccept[allowedType]}
classes={{
base: baseClasses,
idle: idleClasses,
dragging: draggingClasses
}}
/>
)
}
const defaultEmptyContent = (
<>
<div>
<FaImage className="scale-150 mr-8 text-gray-400" />{" "}
<span className="align-middle">Drop your files here</span>
</div>
<p className="mt-4">
or <button className="hover:underline font-bold">Click to Upload</button>{" "}
</p>
</>
);
const defaultDraggingContent = <p className="font-bold text-body2">Drop your files here </p>;
const defaultHasFilesContent = (
<p className="font-bolder">Files Uploaded Successfully!!</p>
);

View File

@@ -1,136 +0,0 @@
import { createAction } from "@reduxjs/toolkit";
import React, { ChangeEvent, useCallback, useRef } from "react"
import { BsUpload } from "react-icons/bs";
import { FaImage } from "react-icons/fa";
import Button from "src/Components/Button/Button"
import { openModal } from "src/redux/features/modals.slice";
import { useAppDispatch } from "src/utils/hooks";
import { useReduxEffect } from "src/utils/hooks/useReduxEffect";
import { UnionToObjectKeys } from "src/utils/types/utils";
import FilesThumbnails from "./FilesThumbnails";
type Props = {
multiple?: boolean;
value?: File[] | string[] | string;
max?: number;
onBlur?: () => void;
onChange?: (files: (File | string)[] | null) => void
uploadBtn?: JSX.Element
uploadText?: string;
allowedType?: 'images';
}
const fileAccept: UnionToObjectKeys<Props, 'allowedType'> = {
images: ".png, .jpg, .jpeg"
} as const;
const fileUrlToObject = async (url: string, fileName: string = 'filename') => {
const res = await fetch(url);
const contentType = res.headers.get('content-type') as string;
const blob = await res.blob()
const file = new File([blob], fileName, { contentType } as any)
return file
}
const INSERT_IMAGE_ACTION = createAction<{ src: string, alt?: string }>('COVER_IMAGE_INSERTED')({ src: '', alt: "" })
const FilesInput = React.forwardRef<any, Props>(({
multiple,
value,
max = 3,
onBlur,
onChange,
allowedType = 'images',
uploadText = 'Upload files',
...props
}, ref) => {
const dispatch = useAppDispatch();
const handleClick = () => {
// ref.current.click();
dispatch(openModal({
Modal: "InsertImageModal",
props: {
callbackAction: {
type: INSERT_IMAGE_ACTION.type,
payload: {
src: "",
alt: ""
}
}
}
}))
}
const onInsertImgUrl = useCallback(({ payload: { src, alt } }: typeof INSERT_IMAGE_ACTION) => {
if (typeof value === 'string')
onChange?.([value, src]);
else
onChange?.([...(value ?? []), src]);
}, [onChange, value])
useReduxEffect(onInsertImgUrl, INSERT_IMAGE_ACTION.type)
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const files = e.target.files && Array.from(e.target.files).slice(0, max);
if (typeof value === 'string')
onChange?.([value, ...(files ?? [])]);
else
onChange?.([...(value ?? []), ...(files ?? [])]);
}
const handleRemove = async (idx: number) => {
if (!value) return onChange?.([]);
if (typeof value === 'string')
onChange?.([]);
else {
let files = [...value]
files.splice(idx, 1);
//change all files urls to file objects
const filesConverted = await Promise.all(files.map(async file => {
if (typeof file === 'string') return await fileUrlToObject(file, "")
else return file;
}))
onChange?.(filesConverted);
}
}
const canUploadMore = multiple ?
!value || (value && value.length < max)
:
!value || value.length === 0
const uploadBtn = props.uploadBtn ?
React.cloneElement(props.uploadBtn, { onClick: handleClick })
:
<Button type='button' onClick={handleClick} ><span className="align-middle">{uploadText}</span> <FaImage className="ml-12 scale-125" /></Button>
return (
<>
<FilesThumbnails files={value} onRemove={handleRemove} />
{
canUploadMore &&
<>
{uploadBtn}
<input
ref={ref}
type="file"
onBlur={onBlur}
style={{ display: 'none' }}
multiple={multiple}
accept={fileAccept[allowedType]}
onChange={handleChange} />
</>
}
</>
)
})
export default FilesInput;

View File

@@ -1,29 +0,0 @@
import { useMemo } from 'react'
import FileThumbnail from './FileThumbnail';
interface Props {
files?: (File | string)[] | string;
onRemove?: (idx: number) => void
}
function processFiles(files: Props['files']) {
if (!files) return [];
if (typeof files === 'string') return [files];
return files;
}
export default function FilesThumbnails({ files, onRemove }: Props) {
const filesConverted = useMemo(() => processFiles(files), [files])
return (
<div className="flex gap-12 mb-12">
{
filesConverted.map((file, idx) => <FileThumbnail
key={idx}
file={file}
onRemove={() => onRemove?.(idx)} />)
}
</div>
)
}

View File

@@ -0,0 +1,29 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react';
import AvatarInput from './AvatarInput';
import { WrapFormController } from 'src/utils/storybook/decorators';
import { ImageType } from '../SingleImageUploadInput/SingleImageUploadInput';
export default {
title: 'Shared/Inputs/Files Inputs/Avatar ',
component: AvatarInput,
decorators: [
WrapFormController<{ avatar: ImageType | null }>({
logValues: true,
name: "avatar",
defaultValues: {
avatar: null
}
})]
} as ComponentMeta<typeof AvatarInput>;
const Template: ComponentStory<typeof AvatarInput> = (args, context) => {
return <AvatarInput {...context.controller} {...args} />
}
export const Default = Template.bind({});
Default.args = {
}

View File

@@ -0,0 +1,105 @@
import { motion } from 'framer-motion';
import React, { ComponentProps, useRef } from 'react'
import { AiOutlineCloudUpload } from 'react-icons/ai';
import { CgArrowsExchangeV } from 'react-icons/cg';
import { FiCamera } from 'react-icons/fi';
import { IoMdClose } from 'react-icons/io';
import { RotatingLines } from 'react-loader-spinner';
import { Nullable } from 'remirror';
import { useIsDraggingOnElement } from 'src/utils/hooks';
import SingleImageUploadInput from '../SingleImageUploadInput/SingleImageUploadInput'
type Value = ComponentProps<typeof SingleImageUploadInput>['value']
interface Props {
width?: number;
isRemovable?: boolean
value: Value;
onChange: (new_value: Nullable<Value>) => void
}
export default function AvatarInput(props: Props) {
const dropAreaRef = useRef<HTMLDivElement>(null!)
const isDragging = useIsDraggingOnElement({ ref: dropAreaRef });
return (
<div
style={{
width: props.width ?? 120,
}}
ref={dropAreaRef}
className='aspect-square rounded-full outline outline-2 outline-gray-200 overflow-hidden cursor-pointer '
>
<SingleImageUploadInput
value={props.value}
onChange={props.onChange}
wrapperClass='rounded-full bg-white h-full'
render={({ img, isUploading, isDraggingOnWindow }) =>
<div className="w-full h-full rounded-full relative group">
{!img &&
<div className='w-full h-full rounded-full bg-white hover:bg-gray-100 flex flex-col justify-center items-center'>
<p className="text-center text-gray-400 text-body2 mb-8"><FiCamera /></p>
<div className={`text-gray-400 text-center text-body5`}>
Add Image
</div>
</div>}
{img &&
<>
<img src={img.url} className='w-full h-full object-cover rounded-full' alt="" />
{!isUploading &&
<div className="flex flex-wrap gap-16 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 ">
<button type='button' className='py-8 px-12 rounded-full bg-black bg-opacity-70 opacity-0 group-hover:opacity-100 hover:bg-opacity-90 transition-opacity text-white text-body1'>
<CgArrowsExchangeV />
</button>
{props.isRemovable && <button type='button' className='py-8 px-12 rounded-full bg-black bg-opacity-70 opacity-0 group-hover:opacity-100 hover:bg-opacity-90 transition-opacity text-white text-body1' onClick={(e) => { e.stopPropagation(); props.onChange(null) }}>
<IoMdClose />
</button>}
</div>
}
</>}
{isUploading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>
}
{isDraggingOnWindow &&
<div
className={
`absolute inset-0 ${isDragging ? 'bg-primary-600' : 'bg-primary-400'} bg-opacity-80 flex flex-col justify-center items-center text-white font-bold transition-transform`
}
>
<motion.div
initial={{ y: 0 }}
animate={
isDragging ? {
y: 5,
transition: {
duration: .4,
repeat: Infinity,
repeatType: 'mirror',
}
} : {
y: 0
}}
className='text-center text-body4'
>
<AiOutlineCloudUpload className="scale-150 text-body2" />
</motion.div>
</div>
}
</div>}
/>
</div>
)
}

View File

@@ -0,0 +1,31 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { WrapFormController } from 'src/utils/storybook/decorators';
import { ImageType } from '../SingleImageUploadInput/SingleImageUploadInput';
import CoverImageInput from './CoverImageInput';
export default {
title: 'Shared/Inputs/Files Inputs/Cover Image ',
component: CoverImageInput,
decorators: [
WrapFormController<{ thumbnail: ImageType | null }>({
logValues: true,
name: "thumbnail",
defaultValues: {
thumbnail: null
}
})]
} as ComponentMeta<typeof CoverImageInput>;
const Template: ComponentStory<typeof CoverImageInput> = (args, context) => {
return <div className="aspect-[5/2] md:aspect-[4/1] rounded-t-16 overflow-hidden">
<CoverImageInput {...context.controller} {...args} />
</div>
}
export const Default = Template.bind({});
Default.args = {
}

View File

@@ -0,0 +1,103 @@
import React, { ComponentProps, useEffect, useRef, useState } from 'react'
import { FaImage } from 'react-icons/fa';
import { CgArrowsExchangeV } from 'react-icons/cg';
import { IoMdClose } from 'react-icons/io';
import { RotatingLines } from 'react-loader-spinner';
import { Nullable } from 'remirror';
import SingleImageUploadInput from '../SingleImageUploadInput/SingleImageUploadInput'
import { motion } from 'framer-motion';
import { AiOutlineCloudUpload } from 'react-icons/ai';
import { useIsDraggingOnElement } from 'src/utils/hooks';
type Value = ComponentProps<typeof SingleImageUploadInput>['value']
interface Props {
value: Value;
rounded?: string;
onChange: (new_value: Nullable<Value>) => void
}
export default function CoverImageInput(props: Props) {
const dropAreaRef = useRef<HTMLDivElement>(null!)
const isDragging = useIsDraggingOnElement({ ref: dropAreaRef });
return (
<div
className='overflow-hidden cursor-pointer w-full h-full'
ref={dropAreaRef}
>
<SingleImageUploadInput
value={props.value}
onChange={props.onChange}
wrapperClass='h-full'
render={({ img, isUploading, isDraggingOnWindow }) =>
<div className="w-full h-full group relative ">
{!img && <div className='w-full h-full flex flex-col justify-center items-center bg-gray-500 outline outline-2 outline-gray-200'>
<p className="text-center text-gray-100 text-body1 md:text-h1 mb-8"><FaImage /></p>
<div className={`text-gray-100 text-center text-body4`}>
Drop a <span className="font-bold">COVER IMAGE</span> here or <br /> <span className="text-blue-300 underline">Click to browse</span>
</div>
</div>}
{img && <>
<img src={img.url} className={`w-full h-full ${props.rounded ?? 'rounded-12'} object-cover`} alt="" />
{!isUploading &&
<div className="flex flex-wrap gap-16 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 ">
<button type='button' className='py-8 px-16 rounded-12 bg-black bg-opacity-70 opacity-0 group-hover:opacity-100 hover:bg-opacity-90 transition-opacity text-white text-h1'>
<CgArrowsExchangeV />
</button>
<button type='button' className='py-8 px-16 rounded-12 bg-black bg-opacity-70 opacity-0 group-hover:opacity-100 hover:bg-opacity-90 transition-opacity text-white text-h1' onClick={(e) => { e.stopPropagation(); props.onChange(null) }}>
<IoMdClose />
</button>
</div>
}
</>}
{isUploading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>
}
{isDraggingOnWindow &&
<div
className={
`absolute inset-0 ${isDragging ? 'bg-primary-600' : 'bg-primary-400'} bg-opacity-80 flex flex-col justify-center items-center text-white font-bold transition-transform`
}
>
<motion.div
initial={{ y: 0 }}
animate={
isDragging ? {
y: 5,
transition: {
duration: .4,
repeat: Infinity,
repeatType: 'mirror',
}
} : {
y: 0
}}
className='text-center text-body1'
>
<AiOutlineCloudUpload className="scale-150 text-h1 mb-16" />
<br />
Drop here to upload
</motion.div>
</div>
}
</div>}
/>
</div>
)
}

View File

@@ -0,0 +1,16 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react';
import FileUploadInput from './FileUploadInput';
export default {
title: 'Shared/Inputs/Files Inputs/Basic',
component: FileUploadInput,
} as ComponentMeta<typeof FileUploadInput>;
const Template: ComponentStory<typeof FileUploadInput> = (args) => <FileUploadInput {...args} />
export const DefaultButton = Template.bind({});
DefaultButton.args = {
}

View File

@@ -0,0 +1,141 @@
import Uploady, { useUploady, useRequestPreSend, UPLOADER_EVENTS, } from "@rpldy/uploady";
import { asUploadButton } from "@rpldy/upload-button";
import Button from "src/Components/Button/Button";
import { fetchUploadUrl } from "../fetch-upload-img-url";
import ImagePreviews from "./ImagePreviews";
import { FaImage } from "react-icons/fa";
import UploadDropZone from "@rpldy/upload-drop-zone";
import { forwardRef, useCallback } from "react";
import styles from './styles.module.scss'
import { MdFileUpload } from "react-icons/md";
import { AiOutlineCloudUpload } from "react-icons/ai";
import { motion } from "framer-motion";
interface Props {
url: string;
}
const UploadBtn = asUploadButton((props: any) => {
useRequestPreSend(async (data) => {
const filename = data.items?.[0].file.name ?? ''
const url = await fetchUploadUrl({ filename });
return {
options: {
destination: {
url
}
}
}
})
// const handleClick = async () => {
// // Make a request to get the url
// try {
// var bodyFormData = new FormData();
// bodyFormData.append('requireSignedURLs', "false");
// const res = await axios({
// url: 'https://cors-anywhere.herokuapp.com/https://api.cloudflare.com/client/v4/accounts/783da4f06e5fdb9012c0632959a6f5b3/images/v2/direct_upload',
// method: 'POST',
// data: bodyFormData,
// headers: {
// "Authorization": "Bearer Xx2-CdsTliYkq6Ayz-1GX4CZubdQVxMwOSDbajP0",
// }
// })
// uploady.upload(res.data.result.uploadUrl, {
// destination: res.data.result.uploadUrl
// })
// } catch (error) {
// console.log(error);
// }
// // make the request with the files
// // uploady.upload()
// }
return <Button {...props} color='primary'>
Upload Image <FaImage className="ml-8 scale-125 align-middle" />
</Button>
});
const DropZone = forwardRef<any, any>((props, ref) => {
const { onClick, ...buttonProps } = props;
useRequestPreSend(async (data) => {
const filename = data.items?.[0].file.name ?? ''
const url = await fetchUploadUrl({ filename });
return {
options: {
destination: {
url
}
}
}
})
const onZoneClick = useCallback(
(e: any) => {
if (onClick) {
onClick(e);
}
},
[onClick]
);
return <UploadDropZone
{...buttonProps}
ref={ref}
onDragOverClassName={styles.active}
extraProps={{ onClick: onZoneClick }}
className={`${styles.zone} border-2 w-full min-h-[200px] max-w-[600px] rounded-16 flex flex-col justify-center items-center text text-body3 border-dashed`}
>
<div className={`${styles.idle_content} text-gray-600`}>
Drop your <span className="font-bold uppercase">IMAGES</span> here or <button className="font-bold text-blue-400 underline">Click to browse</button>
</div>
<motion.div
animate={{
y: 5,
}}
transition={{
duration: .5,
repeat: Infinity,
repeatType: 'mirror'
}}
className={`${styles.active_content} text-white font-bold`}>
Drop it to upload <AiOutlineCloudUpload className="scale-150 text-body1 ml-16" />
</motion.div>
</UploadDropZone>
})
const DropZoneButton = asUploadButton(DropZone);
export default function FileUploadInput(props: Props) {
return (
<Uploady
multiple={true}
inputFieldName='file'
grouped={false}
listeners={{
[UPLOADER_EVENTS.ITEM_FINISH]: (item) => {
const { id, filename, variants } = item?.uploadResponse?.data?.result ?? {}
if (id) {
console.log(id, filename, variants);
}
}
}}
>
<DropZoneButton />
{/* <UploadBtn /> */}
<ImagePreviews />
</Uploady>
)
}

View File

@@ -0,0 +1,92 @@
import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview'
import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady';
import React, { useState } from 'react'
import { RotatingLines } from 'react-loader-spinner';
export default function ImagePreviews() {
return (
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-16 mt-24">
<UploadPreview PreviewComponent={CustomImagePreview} rememberPreviousBatches />
</div>
)
}
function CustomImagePreview({ id, url }: PreviewComponentProps) {
const [progress, setProgress] = useState<number>(0);
const [itemState, setItemState] = useState<string>(STATES.PROGRESS);
useItemProgressListener(item => {
if (item.completed > progress) {
setProgress(() => item.completed);
if (item.completed === 100) {
setItemState(STATES.DONE)
} else {
setItemState(STATES.PROGRESS)
}
}
}, id);
useItemAbortListener(item => {
setItemState(STATES.CANCELLED);
}, id);
useItemCancelListener(item => {
setItemState(STATES.CANCELLED);
}, id);
useItemErrorListener(item => {
setItemState(STATES.ERROR);
}, id);
return <div className="aspect-video relative rounded-12 md:rounded-16 overflow-hidden border-2 border-gray-200">
<img src={url}
className={`
w-full h-full object-cover
${itemState === STATES.PROGRESS && 'opacity-50'}
`}
alt="" />
<div className="text-body5 absolute inset-0"
>
</div>
{itemState === STATES.PROGRESS &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>}
{itemState === STATES.ERROR &&
<div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
Failed...
</div>}
{itemState === STATES.CANCELLED &&
<div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
Cancelled
</div>}
</div>;
};
const STATES = {
PROGRESS: "PROGRESS",
DONE: "DONE",
CANCELLED: "CANCELLED",
ERROR: "ERROR"
};
const STATE_COLORS = {
[STATES.PROGRESS]: "#f4e4a4",
[STATES.DONE]: "#a5f7b3",
[STATES.CANCELLED]: "#f7cdcd",
[STATES.ERROR]: "#ee4c4c"
};

View File

@@ -0,0 +1,25 @@
.zone {
background-color: #f2f4f7;
border-color: #e4e7ec;
.active_content {
display: none;
}
.idle_content {
display: block;
}
&.active {
background-color: #b3a0ff;
border-color: #9e88ff;
.active_content {
display: block;
}
.idle_content {
display: none;
}
}
}

View File

@@ -0,0 +1,97 @@
import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview'
import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady';
import { useState } from 'react'
import ScreenShotsThumbnail from './ScreenshotThumbnail'
export default function ImagePreviews() {
return (
<UploadPreview PreviewComponent={CustomImagePreview} rememberPreviousBatches />
)
}
function CustomImagePreview({ id, url }: PreviewComponentProps) {
const [progress, setProgress] = useState<number>(0);
const [itemState, setItemState] = useState<string>(STATES.PROGRESS);
const abortItem = useAbortItem();
useItemProgressListener(item => {
if (item.completed > progress) {
setProgress(() => item.completed);
if (item.completed === 100) {
setItemState(STATES.DONE)
} else {
setItemState(STATES.PROGRESS)
}
}
}, id);
useItemAbortListener(item => {
setItemState(STATES.CANCELLED);
}, id);
useItemCancelListener(item => {
setItemState(STATES.CANCELLED);
}, id);
useItemErrorListener(item => {
setItemState(STATES.ERROR);
}, id);
if (itemState === STATES.DONE || itemState === STATES.CANCELLED)
return null
return <ScreenShotsThumbnail
url={url}
isLoading={itemState === STATES.PROGRESS}
isError={itemState === STATES.ERROR}
onCancel={() => {
abortItem(id)
}}
/>
// return <div className="aspect-video relative rounded-12 md:rounded-16 overflow-hidden border-2 border-gray-200">
// <img src={url}
// className={`
// w-full h-full object-cover
// ${itemState === STATES.PROGRESS && 'opacity-50'}
// `}
// alt="" />
// <div className="text-body5 absolute inset-0"
// >
// </div>
// {itemState === STATES.PROGRESS &&
// <div
// className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
// >
// <RotatingLines
// strokeColor="#fff"
// strokeWidth="3"
// animationDuration="0.75"
// width="48"
// visible={true}
// />
// </div>}
// {itemState === STATES.ERROR &&
// <div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
// Failed...
// </div>}
// {itemState === STATES.CANCELLED &&
// <div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
// Cancelled
// </div>}
// </div>;
};
const STATES = {
PROGRESS: "PROGRESS",
DONE: "DONE",
CANCELLED: "CANCELLED",
ERROR: "ERROR"
};

View File

@@ -0,0 +1,52 @@
import React from 'react'
import { FaTimes } from 'react-icons/fa';
import { RotatingLines } from 'react-loader-spinner';
interface Props {
url?: string,
isLoading?: boolean;
isError?: boolean;
onCancel?: () => void;
}
export default function ScreenshotThumbnail({ url, isLoading, isError, onCancel }: Props) {
const isEmpty = !url;
return (
<div className={`
aspect-video relative rounded-16 md:rounded-14 overflow-hidden border-2 border-gray-200
${isEmpty && "border-dashed"}
`}>
{!isEmpty && <img src={url}
className={`
w-full h-full object-cover
${isLoading && 'opacity-50'}
`}
alt="" />}
<div className="text-body5 absolute inset-0"
>
</div>
{isLoading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>}
{isError &&
<div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
Failed...
</div>}
{!isEmpty &&
<button className="absolute bg-gray-900 hover:bg-opacity-100 bg-opacity-60 text-white rounded-full w-32 h-32 top-8 right-8" onClick={() => onCancel?.()}><FaTimes /></button>
}
</div>
)
}

View File

@@ -0,0 +1,84 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react';
import ScreenshotsInput, { ScreenshotType } from './ScreenshotsInput';
import { WrapForm, WrapFormController } from 'src/utils/storybook/decorators';
export default {
title: 'Shared/Inputs/Files Inputs/Screenshots',
component: ScreenshotsInput,
decorators: [
WrapFormController<{ screenshots: Array<ScreenshotType> }>({
logValues: true,
name: "screenshots",
defaultValues: {
screenshots: []
}
})]
} as ComponentMeta<typeof ScreenshotsInput>;
const Template: ComponentStory<typeof ScreenshotsInput> = (args, context) => {
return <ScreenshotsInput {...context.controller} {...args} />
}
export const Empty = Template.bind({});
Empty.args = {
}
export const WithValues = Template.bind({});
WithValues.decorators = [
WrapFormController<{ screenshots: Array<ScreenshotType> }>({
logValues: true,
name: "screenshots",
defaultValues: {
screenshots: [{
id: '123',
name: 'tree',
url: "https://picsum.photos/id/1021/800/800.jpg"
},
{
id: '555',
name: 'whatever',
url: "https://picsum.photos/id/600/800/800.jpg"
},]
}
}) as any
];
WithValues.args = {
}
export const Full = Template.bind({});
Full.decorators = [
WrapFormController<{ screenshots: Array<ScreenshotType> }>({
logValues: true,
name: "screenshots",
defaultValues: {
screenshots: [
{
id: '123',
name: 'tree',
url: "https://picsum.photos/id/1021/800/800.jpg"
},
{
id: '555',
name: 'whatever',
url: "https://picsum.photos/id/600/800/800.jpg"
},
{
id: '562',
name: 'Moon',
url: "https://picsum.photos/id/32/800/800.jpg"
},
{
id: '342',
name: 'Sun',
url: "https://picsum.photos/id/523/800/800.jpg"
},
]
}
}) as any
];
Full.args = {
}

View File

@@ -0,0 +1,151 @@
import Uploady, { useRequestPreSend, UPLOADER_EVENTS } from "@rpldy/uploady";
import { asUploadButton } from "@rpldy/upload-button";
// import { fetchUploadUrl } from "./fetch-upload-img-url";
import ImagePreviews from "./ImagePreviews";
import UploadDropZone from "@rpldy/upload-drop-zone";
import { forwardRef, useCallback, useState } from "react";
import styles from './styles.module.scss'
import { AiOutlineCloudUpload } from "react-icons/ai";
import { motion } from "framer-motion";
import { getMockSenderEnhancer } from "@rpldy/mock-sender";
import ScreenshotThumbnail from "./ScreenshotThumbnail";
import { FiCamera } from "react-icons/fi";
import { Control, Path, useController } from "react-hook-form";
const mockSenderEnhancer = getMockSenderEnhancer({
delay: 1500,
});
const MAX_UPLOAD_COUNT = 4 as const;
export interface ScreenshotType {
id: string,
name: string,
url: string;
}
interface Props {
value: ScreenshotType[],
onChange: (new_value: ScreenshotType[]) => void
}
export default function ScreenshotsInput(props: Props) {
const { value: uploadedFiles, onChange } = props;
const [uploadingCount, setUploadingCount] = useState(0)
const canUploadMore = uploadingCount + uploadedFiles.length < MAX_UPLOAD_COUNT;
const placeholdersCount = (MAX_UPLOAD_COUNT - (uploadingCount + uploadedFiles.length + 1));
return (
<Uploady
multiple={true}
inputFieldName='file'
grouped={false}
enhancer={mockSenderEnhancer}
listeners={{
[UPLOADER_EVENTS.BATCH_ADD]: (batch) => {
setUploadingCount(v => v + batch.items.length)
},
[UPLOADER_EVENTS.ITEM_FINALIZE]: () => setUploadingCount(v => v - 1),
[UPLOADER_EVENTS.ITEM_FINISH]: (item) => {
// Just for mocking purposes
const dataUrl = URL.createObjectURL(item.file);
const { id, filename, variants } = item?.uploadResponse?.data?.result ?? {
id: Math.random().toString(),
filename: item.file.name,
variants: [
"",
dataUrl
]
}
if (id) {
onChange([...uploadedFiles, { id, name: filename, url: variants[1] }].slice(-MAX_UPLOAD_COUNT))
}
}
}}
>
<div className="grid grid-cols-[repeat(auto-fit,minmax(200px,1fr))] gap-16 mt-24">
{canUploadMore && <DropZoneButton />}
{uploadedFiles.map(f => <ScreenshotThumbnail
key={f.id}
url={f.url}
onCancel={() => {
onChange(uploadedFiles.filter(file => file.id !== f.id))
}} />)}
<ImagePreviews />
{(placeholdersCount > 0) &&
Array(placeholdersCount).fill(0).map((_, idx) => <ScreenshotThumbnail key={idx} />)}
</div>
</Uploady>
)
}
const DropZone = forwardRef<any, any>((props, ref) => {
const { onClick, ...buttonProps } = props;
useRequestPreSend(async (data) => {
const filename = data.items?.[0].file.name ?? ''
// const url = await fetchUploadUrl({ filename });
return {
options: {
destination: {
url: "URL"
}
}
}
})
const onZoneClick = useCallback(
(e: any) => {
if (onClick) {
onClick(e);
}
},
[onClick]
);
return <UploadDropZone
{...buttonProps}
ref={ref}
onDragOverClassName={styles.active}
extraProps={{ onClick: onZoneClick }}
className={`${styles.zone} aspect-video relative rounded-16 md:rounded-14 overflow-hidden border-2 border-gray-200 flex flex-col justify-center items-center cursor-pointer border-dashed`}
>
<div className={styles.idle_content}>
<p className="text-center text-gray-400 text-body1 mb-8"><FiCamera /></p>
<div className={`text-gray-600 text-center text-body4`}>
<span className="text-blue-500 underline">Browse images</span> or <br /> <span className="text-blue-500">drop </span>
them here
</div>
</div>
<motion.div
animate={{
y: 5,
}}
transition={{
duration: .5,
repeat: Infinity,
repeatType: 'mirror'
}}
className={`${styles.active_content} text-white font-bold text-center`}>
Drop to upload <br /> <AiOutlineCloudUpload className="scale-150 text-body1 mt-16" />
</motion.div>
</UploadDropZone>
})
const DropZoneButton = asUploadButton(DropZone);

View File

@@ -0,0 +1,25 @@
.zone {
background-color: #f2f4f7;
border-color: #e4e7ec;
.active_content {
display: none;
}
.idle_content {
display: block;
}
&.active {
background-color: #b3a0ff;
border-color: #9e88ff;
.active_content {
display: block;
}
.idle_content {
display: none;
}
}
}

View File

@@ -0,0 +1,97 @@
import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview'
import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady';
import { useState } from 'react'
import ScreenShotsThumbnail from './ScreenshotThumbnail'
export default function ImagePreviews() {
return (
<UploadPreview PreviewComponent={CustomImagePreview} rememberPreviousBatches />
)
}
function CustomImagePreview({ id, url }: PreviewComponentProps) {
const [progress, setProgress] = useState<number>(0);
const [itemState, setItemState] = useState<string>(STATES.PROGRESS);
const abortItem = useAbortItem();
useItemProgressListener(item => {
if (item.completed > progress) {
setProgress(() => item.completed);
if (item.completed === 100) {
setItemState(STATES.DONE)
} else {
setItemState(STATES.PROGRESS)
}
}
}, id);
useItemAbortListener(item => {
setItemState(STATES.CANCELLED);
}, id);
useItemCancelListener(item => {
setItemState(STATES.CANCELLED);
}, id);
useItemErrorListener(item => {
setItemState(STATES.ERROR);
}, id);
if (itemState === STATES.DONE || itemState === STATES.CANCELLED)
return null
return <ScreenShotsThumbnail
url={url}
isLoading={itemState === STATES.PROGRESS}
isError={itemState === STATES.ERROR}
onCancel={() => {
abortItem(id)
}}
/>
// return <div className="aspect-video relative rounded-12 md:rounded-16 overflow-hidden border-2 border-gray-200">
// <img src={url}
// className={`
// w-full h-full object-cover
// ${itemState === STATES.PROGRESS && 'opacity-50'}
// `}
// alt="" />
// <div className="text-body5 absolute inset-0"
// >
// </div>
// {itemState === STATES.PROGRESS &&
// <div
// className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
// >
// <RotatingLines
// strokeColor="#fff"
// strokeWidth="3"
// animationDuration="0.75"
// width="48"
// visible={true}
// />
// </div>}
// {itemState === STATES.ERROR &&
// <div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
// Failed...
// </div>}
// {itemState === STATES.CANCELLED &&
// <div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
// Cancelled
// </div>}
// </div>;
};
const STATES = {
PROGRESS: "PROGRESS",
DONE: "DONE",
CANCELLED: "CANCELLED",
ERROR: "ERROR"
};

View File

@@ -0,0 +1,52 @@
import React from 'react'
import { FaTimes } from 'react-icons/fa';
import { RotatingLines } from 'react-loader-spinner';
interface Props {
url?: string,
isLoading?: boolean;
isError?: boolean;
onCancel?: () => void;
}
export default function ScreenshotThumbnail({ url, isLoading, isError, onCancel }: Props) {
const isEmpty = !url;
return (
<div className={`
aspect-video relative rounded-16 md:rounded-14 overflow-hidden border-2 border-gray-200
${isEmpty && "border-dashed"}
`}>
{!isEmpty && <img src={url}
className={`
w-full h-full object-cover
${isLoading && 'opacity-50'}
`}
alt="" />}
<div className="text-body5 absolute inset-0"
>
</div>
{isLoading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>}
{isError &&
<div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
Failed...
</div>}
{!isEmpty &&
<button className="absolute bg-gray-900 hover:bg-opacity-100 bg-opacity-60 text-white rounded-full w-32 h-32 top-8 right-8" onClick={() => onCancel?.()}><FaTimes /></button>
}
</div>
)
}

View File

@@ -0,0 +1,28 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react';
import SingleImageUploadInput, { ImageType } from './SingleImageUploadInput';
import { WrapFormController } from 'src/utils/storybook/decorators';
import { RotatingLines } from 'react-loader-spinner';
import { FiCamera, } from 'react-icons/fi';
import { FaExchangeAlt, FaImage } from 'react-icons/fa';
export default {
title: 'Shared/Inputs/Files Inputs/Single Image Upload ',
component: SingleImageUploadInput,
decorators: [
WrapFormController<{ avatar: ImageType | null }>({
logValues: true,
name: "avatar",
defaultValues: {
avatar: null
}
})]
} as ComponentMeta<typeof SingleImageUploadInput>;
const Template: ComponentStory<typeof SingleImageUploadInput> = (args, context) => {
return <SingleImageUploadInput {...context.controller} {...args} />
}

View File

@@ -0,0 +1,141 @@
import Uploady, { useRequestPreSend, UPLOADER_EVENTS, useAbortAll } from "@rpldy/uploady";
import { asUploadButton } from "@rpldy/upload-button";
// import { fetchUploadUrl } from "./fetch-upload-img-url";
import UploadDropZone from "@rpldy/upload-drop-zone";
import { forwardRef, ReactElement, useCallback, useState } from "react";
import styles from './styles.module.scss'
import { getMockSenderEnhancer } from "@rpldy/mock-sender";
import { NotificationsService } from "src/services";
import { useIsDraggingOnElement } from 'src/utils/hooks';
import { fetchUploadImageUrl } from "src/api/uploading";
const mockSenderEnhancer = getMockSenderEnhancer({
delay: 1500,
});
export interface ImageType {
id?: string | null,
name?: string | null,
url: string;
}
type RenderPropArgs = {
isUploading?: boolean;
img: ImageType | null,
onAbort: () => void,
isDraggingOnWindow?: boolean
}
interface Props {
value: ImageType | null | undefined,
onChange: (new_value: ImageType | null) => void;
wrapperClass?: string;
render: (args: RenderPropArgs) => ReactElement;
}
export default function SingleImageUploadInput(props: Props) {
const { value, onChange, render } = props;
const [currentlyUploadingItem, setCurrentlyUploadingItem] = useState<ImageType | null>(null)
return (
<Uploady
accept="image/*"
inputFieldName='file'
grouped={false}
listeners={{
[UPLOADER_EVENTS.ITEM_START]: (item) => {
onChange(null)
setCurrentlyUploadingItem({
id: item.id,
url: URL.createObjectURL(item.file),
name: item.file.name,
})
},
[UPLOADER_EVENTS.ITEM_ERROR]: (item) => {
NotificationsService.error("An error happened while uploading. Please try again.")
},
[UPLOADER_EVENTS.ITEM_FINALIZE]: () => setCurrentlyUploadingItem(null),
[UPLOADER_EVENTS.ITEM_FINISH]: (item) => {
const { id, filename, variants } = item?.uploadResponse?.data?.result;
const url = (variants as string[]).find(v => v.includes('public'));
if (id && url) {
onChange({ id, name: filename, url, })
}
}
}}
>
<DropZoneButton
extraProps={{
renderProps: {
isUploading: !!currentlyUploadingItem,
img: currentlyUploadingItem ?? value ?? null,
render,
wrapperClass: props.wrapperClass
}
}
}
/>
</Uploady>
)
}
const DropZone = forwardRef<any, any>((props, ref) => {
const { onClick, children, renderProps, ...buttonProps } = props;
const isDraggingOnWindow = useIsDraggingOnElement()
useRequestPreSend(async (data) => {
const filename = data.items?.[0].file.name ?? ''
const res = await fetchUploadImageUrl({ filename });
return {
options: {
destination: {
url: res.uploadURL
},
}
}
})
const onZoneClick = useCallback(
(e: any) => {
if (onClick) {
onClick(e);
}
},
[onClick]
);
return <UploadDropZone
{...buttonProps}
ref={ref}
type='button'
onDragOverClassName={'drag-active'}
extraProps={{ onClick: onZoneClick }}
className={renderProps.wrapperClass}
>
{renderProps.render({
img: renderProps.img,
isUploading: renderProps.isUploading,
isDraggingOnWindow,
})}
</UploadDropZone>
})
const DropZoneButton = asUploadButton(DropZone);

View File

@@ -0,0 +1,25 @@
.zone {
background-color: #f2f4f7;
border-color: #e4e7ec;
.active_content {
display: none;
}
.idle_content {
display: block;
}
&.active {
background-color: #b3a0ff;
border-color: #9e88ff;
.active_content {
display: block;
}
.idle_content {
display: none;
}
}
}

View File

@@ -0,0 +1,29 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react';
import ThumbnailInput from './ThumbnailInput';
import { WrapFormController } from 'src/utils/storybook/decorators';
import { ImageType } from '../SingleImageUploadInput/SingleImageUploadInput';
export default {
title: 'Shared/Inputs/Files Inputs/Thumbnail ',
component: ThumbnailInput,
decorators: [
WrapFormController<{ thumbnail: ImageType | null }>({
logValues: true,
name: "thumbnail",
defaultValues: {
thumbnail: null
}
})]
} as ComponentMeta<typeof ThumbnailInput>;
const Template: ComponentStory<typeof ThumbnailInput> = (args, context) => {
return <ThumbnailInput {...context.controller} {...args} />
}
export const Default = Template.bind({});
Default.args = {
}

View File

@@ -0,0 +1,54 @@
import React, { ComponentProps } from 'react'
import { FiCamera } from 'react-icons/fi';
import { RotatingLines } from 'react-loader-spinner';
import { Nullable } from 'remirror';
import SingleImageUploadInput from '../SingleImageUploadInput/SingleImageUploadInput'
type Value = ComponentProps<typeof SingleImageUploadInput>['value']
interface Props {
width?: number
value: Value;
onChange: (new_value: Nullable<Value>) => void
}
export default function ThumbnailInput(props: Props) {
return (
<div
style={{
width: props.width ?? 120,
}}
className='aspect-square rounded-16 outline outline-2 outline-gray-200 overflow-hidden cursor-pointer bg-white hover:bg-gray-100'
>
<SingleImageUploadInput
value={props.value}
onChange={props.onChange}
wrapperClass='h-full'
render={({ img, isUploading }) => <div className="w-full h-full relative flex flex-col justify-center items-center">
{img && <img src={img.url} className='w-full h-full object-cover rounded-16' alt="" />}
{!img &&
<>
<p className="text-center text-gray-400 text-body2 mb-8"><FiCamera /></p>
<div className={`text-gray-400 text-center text-body5`}>
Add Image
</div>
</>}
{isUploading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>
}
</div>}
/>
</div>
)
}

View File

@@ -0,0 +1,25 @@
import axios from "axios";
import { NotificationsService } from "src/services";
export async function fetchUploadUrl(options?: Partial<{ filename: string }>) {
const { filename } = options ?? {}
try {
const bodyFormData = new FormData();
bodyFormData.append('requireSignedURLs', "false");
const res = await axios({
url: 'https://cors-anywhere.herokuapp.com/https://api.cloudflare.com/client/v4/accounts/783da4f06e5fdb9012c0632959a6f5b3/images/v2/direct_upload',
method: 'POST',
data: bodyFormData,
headers: {
"Authorization": "Bearer XXX",
}
})
return res.data.result.uploadURL as string;
} catch (error) {
console.log(error);
NotificationsService.error("A network error happened.")
return "couldnt fetch upload url";
}
}

View File

@@ -13,7 +13,9 @@ interface Option {
readonly description: string | null readonly description: string | null
} }
type Tag = Omit<OfficialTagsQuery['officialTags'][number], 'id'> type Tag = Omit<OfficialTagsQuery['officialTags'][number], 'id'>
type Value = { title: Tag['title'] }
interface Props { interface Props {
classes?: { classes?: {
@@ -22,11 +24,80 @@ interface Props {
} }
placeholder?: string placeholder?: string
max?: number; max?: number;
value: Value[];
onChange?: (new_value: Value[]) => void;
onBlur?: () => void;
[k: string]: any [k: string]: any
} }
export default function TagsInput({
classes,
placeholder = 'Write some tags',
max = 5,
value,
onChange,
onBlur,
...props }: Props) {
const officalTags = useOfficialTagsQuery();
const handleChange = (newValue: OnChangeValue<Option, true>,) => {
onChange?.([...newValue.map(transformer.optionToTag)]);
onBlur?.();
}
const maxReached = value.length >= max;
const currentPlaceholder = maxReached ? '' : value.length > 0 ? "Add Another..." : placeholder;
const tagsOptions = !maxReached ? (officalTags.data?.officialTags ?? []).filter(t => !value.some((v) => v.title === t.title)).map(transformer.tagToOption) : [];
return (
<div className={`${classes?.container}`}>
<Select
isLoading={officalTags.loading}
options={tagsOptions}
isMulti
isOptionDisabled={() => maxReached}
placeholder={currentPlaceholder}
noOptionsMessage={() => {
return maxReached
? "You've reached the max number of tags."
: "No tags available";
}}
closeMenuOnSelect={false}
value={value.map(transformer.valueToOption)}
onChange={handleChange as any}
onBlur={onBlur}
components={{
Option: OptionComponent,
// ValueContainer: CustomValueContainer
}}
styles={colourStyles as any}
theme={(theme) => ({
...theme,
borderRadius: 8,
colors: {
...theme.colors,
primary: 'var(--primary)',
},
})}
/>
{/* <div className="flex mt-16 gap-8 flex-wrap">
{(value as Tag[]).map((tag, idx) => <Badge color="gray" key={tag.title} onRemove={() => handleRemove(idx)} >{tag.title}</Badge>)}
</div> */}
</div>
)
}
const transformer = { const transformer = {
valueToOption: (tag: Value): Option => ({ label: tag.title, value: tag.title, icon: null, description: null }),
tagToOption: (tag: Tag): Option => ({ label: tag.title, value: tag.title, icon: tag.icon, description: tag.description }), tagToOption: (tag: Tag): Option => ({ label: tag.title, value: tag.title, icon: tag.icon, description: tag.description }),
optionToTag: (o: Option): Tag => ({ title: o.value, icon: o.icon, description: o.description, }) optionToTag: (o: Option): Tag => ({ title: o.value, icon: o.icon, description: o.description, })
} }
@@ -107,75 +178,3 @@ const colourStyles: StylesConfig = {
paddingRight: 0, paddingRight: 0,
}) })
} }
export default function TagsInput({
classes,
placeholder = 'Write some tags',
max = 5,
...props }: Props) {
const officalTags = useOfficialTagsQuery();
const { field: { value, onChange, onBlur } } = useController({
name: props.name ?? "tags",
control: props.control,
})
const handleChange = (newValue: OnChangeValue<Option, true>,) => {
onChange([...newValue.map(transformer.optionToTag)]);
onBlur();
}
const handleRemove = (idx: number) => {
onChange((value as Tag[]).filter((_, i) => idx !== i))
onBlur();
}
const maxReached = value.length >= max;
const currentPlaceholder = maxReached ? '' : value.length > 0 ? "Add Another..." : placeholder;
const tagsOptions = !maxReached ? (officalTags.data?.officialTags ?? []).filter(t => !value.some((v: Tag) => v.title === t.title)).map(transformer.tagToOption) : [];
return (
<div className={`${classes?.container}`}>
<Select
isLoading={officalTags.loading}
options={tagsOptions}
isMulti
isOptionDisabled={() => maxReached}
placeholder={currentPlaceholder}
noOptionsMessage={() => {
return maxReached
? "You've reached the max number of tags."
: "No tags available";
}}
closeMenuOnSelect={false}
value={value.map(transformer.tagToOption)}
onChange={handleChange as any}
onBlur={onBlur}
components={{
Option: OptionComponent,
// ValueContainer: CustomValueContainer
}}
styles={colourStyles as any}
theme={(theme) => ({
...theme,
borderRadius: 8,
colors: {
...theme.colors,
primary: 'var(--primary)',
},
})}
/>
{/* <div className="flex mt-16 gap-8 flex-wrap">
{(value as Tag[]).map((tag, idx) => <Badge color="gray" key={tag.title} onRemove={() => handleRemove(idx)} >{tag.title}</Badge>)}
</div> */}
</div>
)
}

View File

@@ -1,91 +0,0 @@
import React, { FormEvent, useState } from 'react'
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
import { motion } from 'framer-motion'
import { IoClose } from 'react-icons/io5'
import Button from 'src/Components/Button/Button'
import { useAppDispatch } from 'src/utils/hooks'
import { PayloadAction } from '@reduxjs/toolkit'
interface Props extends ModalCard {
callbackAction: PayloadAction<{ src: string, alt?: string }>
}
export default function InsertImageModal({ onClose, direction, callbackAction, ...props }: Props) {
const [urlInput, setUrlInput] = useState("")
const [altInput, setAltInput] = useState("")
const dispatch = useAppDispatch();
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
if (urlInput.length > 10) {
// onInsert({ src: urlInput, alt: altInput })
const action = Object.assign({}, callbackAction);
action.payload = { src: urlInput, alt: altInput }
dispatch(action)
onClose?.();
}
}
return (
<motion.div
custom={direction}
variants={modalCardVariants}
initial='initial'
animate="animate"
exit='exit'
className="modal-card max-w-[660px] p-24 rounded-xl relative"
>
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
<h2 className='text-h5 font-bold'>Add Image</h2>
<form onSubmit={handleSubmit}>
<div className="grid md:grid-cols-3 gap-16 mt-32">
<div className='md:col-span-2'>
<p className="text-body5">
Image URL
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
value={urlInput}
onChange={e => setUrlInput(e.target.value)}
placeholder='https://images.com/my-image'
/>
</div>
</div>
<div>
<p className="text-body5">
Alt Text
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
value={altInput}
onChange={e => setAltInput(e.target.value)}
placeholder=''
/>
</div>
</div>
</div>
<div className="mt-32 w-1/2 mx-auto aspect-video bg-gray-200 rounded-10">
{urlInput && <img
src={urlInput}
className='w-full h-full object-cover rounded-10'
alt={altInput}
/>}
</div>
<div className="flex gap-16 justify-end mt-32">
<Button onClick={onClose}>
Cancel
</Button>
<Button type='submit' color='primary' >
Add
</Button>
</div>
</form>
</motion.div>
)
}

View File

@@ -5,7 +5,7 @@ import InsertImageModal from './InsertImageModal';
import { ModalsDecorator } from 'src/utils/storybook/decorators'; import { ModalsDecorator } from 'src/utils/storybook/decorators';
export default { export default {
title: 'Shared/Inputs/Text Editor/Insert Image Modal', title: 'Shared/Inputs/Files Inputs/Image Modal',
component: InsertImageModal, component: InsertImageModal,
decorators: [ModalsDecorator] decorators: [ModalsDecorator]
@@ -14,4 +14,13 @@ export default {
const Template: ComponentStory<typeof InsertImageModal> = (args) => <InsertImageModal {...args} />; const Template: ComponentStory<typeof InsertImageModal> = (args) => <InsertImageModal {...args} />;
export const Default = Template.bind({}); export const Default = Template.bind({});
Default.args = {
callbackAction: {
type: "INSERT_IMAGE_IN_STORY",
payload: {
src: "",
alt: "",
}
}
}

View File

@@ -0,0 +1,176 @@
import React, { FormEvent, useRef, useState } from 'react'
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
import { motion } from 'framer-motion'
import { IoClose } from 'react-icons/io5'
import Button from 'src/Components/Button/Button'
import { useAppDispatch, useIsDraggingOnElement } from 'src/utils/hooks'
import { PayloadAction } from '@reduxjs/toolkit'
import { RotatingLines } from 'react-loader-spinner'
import { FaExchangeAlt, FaImage } from 'react-icons/fa'
import SingleImageUploadInput, { ImageType } from 'src/Components/Inputs/FilesInputs/SingleImageUploadInput/SingleImageUploadInput'
import { AiOutlineCloudUpload } from 'react-icons/ai'
interface Props extends ModalCard {
callbackAction: PayloadAction<{ src: string, alt?: string }>
}
export default function InsertImageModal({ onClose, direction, callbackAction, ...props }: Props) {
const [uploadedImage, setUploadedImage] = useState<ImageType | null>(null)
const [altInput, setAltInput] = useState("")
const dispatch = useAppDispatch();
const dropAreaRef = useRef<HTMLDivElement>(null!)
const isDragging = useIsDraggingOnElement({ ref: dropAreaRef });
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
console.log(uploadedImage?.url);
if (uploadedImage?.url) {
// onInsert({ src: urlInput, alt: altInput })
const action = Object.assign({}, callbackAction);
action.payload = { src: uploadedImage.url, alt: altInput }
dispatch(action)
onClose?.();
}
}
return (
<motion.div
custom={direction}
variants={modalCardVariants}
initial='initial'
animate="animate"
exit='exit'
className="modal-card max-w-[660px] p-24 rounded-xl relative"
>
<IoClose className='absolute text-body2 top-24 right-24 hover:cursor-pointer' onClick={onClose} />
<h2 className='text-h5 font-bold'>Add Image</h2>
<form onSubmit={handleSubmit}>
{/* <div className="grid md:grid-cols-3 gap-16 mt-32">
<div className='md:col-span-2'>
<p className="text-body5">
Image URL
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
value={urlInput}
onChange={e => setUrlInput(e.target.value)}
placeholder='https://images.com/my-image'
/>
</div>
</div>
<div>
<p className="text-body5">
Alt Text
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
value={altInput}
onChange={e => setAltInput(e.target.value)}
placeholder=''
/>
</div>
</div>
</div>
<div className="mt-32 w-1/2 mx-auto aspect-video bg-gray-200 rounded-10">
{urlInput && <img
src={urlInput}
className='w-full h-full object-cover rounded-10'
alt={altInput}
/>}
</div> */}
<SingleImageUploadInput
value={uploadedImage}
onChange={setUploadedImage}
wrapperClass='h-full mt-32'
render={({ img, isUploading, isDraggingOnWindow }) => <div ref={dropAreaRef} className="w-full group aspect-video bg-gray-100 cursor-pointer rounded-16 border-2 border-gray200 overflow-hidden relative flex flex-col justify-center items-center">
{img && <>
<img src={img.url} className='w-full h-full object-cover rounded-16' alt="" />
{!isUploading &&
<button type='button' className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 py-16 px-24 rounded-12 bg-black bg-opacity-70 opacity-0 group-hover:opacity-100 hover:bg-opacity-90 transition-opacity text-white text-h3'>
<FaExchangeAlt />
</button>}
</>}
{!img &&
<>
<p className="text-center text-gray-700 text-body1 md:text-h1 mb-8"><FaImage /></p>
<div className={`text-gray-600 text-center text-body4`}>
Drop an <span className="font-bold">IMAGE</span> here or <br /> <span className="text-blue-500 underline">Click to browse</span>
</div>
</>}
{isUploading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>
}
{isDraggingOnWindow &&
<div
className={
`absolute inset-0 ${isDragging ? 'bg-primary-600' : 'bg-primary-400'} bg-opacity-80 flex flex-col justify-center items-center text-white font-bold transition-transform`
}
>
<motion.div
initial={{ y: 0 }}
animate={
isDragging ? {
y: 5,
transition: {
duration: .4,
repeat: Infinity,
repeatType: 'mirror',
}
} : {
y: 0
}}
className='text-center text-body1'
>
<AiOutlineCloudUpload className="scale-150 text-h1 mb-16" />
<br />
Drop here to upload
</motion.div>
</div>
}
</div>}
/>
<div className='mt-24'>
<p className="text-body5">
Alternative Text
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
value={altInput}
onChange={e => setAltInput(e.target.value)}
placeholder='A description for the content of this image'
/>
</div>
</div>
<div className="flex gap-16 justify-end mt-32">
<Button onClick={onClose}>
Cancel
</Button>
<Button type='submit' color='primary' >
Add
</Button>
</div>
</form>
</motion.div>
)
}

21
src/api/auth.ts Normal file
View File

@@ -0,0 +1,21 @@
import { CONSTS } from "src/utils";
export async function fetchLnurlAuth() {
const res = await fetch(CONSTS.apiEndpoint + '/get-login-url', {
credentials: 'include'
})
const data = await res.json()
return data;
}
export async function fetchIsLoggedIn(session_token: string) {
const res = await fetch(CONSTS.apiEndpoint + '/is-logged-in', {
credentials: 'include',
headers: {
session_token
}
});
const data = await res.json();
return data.logged_in;
}

12
src/api/uploading.ts Normal file
View File

@@ -0,0 +1,12 @@
import axios from "axios";
import { CONSTS } from "src/utils";
export async function fetchUploadImageUrl({ filename }: { filename: string }) {
const res = await axios.post(CONSTS.apiEndpoint + '/upload-image-url', {
filename
}, {
withCredentials: true
})
return res.data;
}

View File

@@ -10,6 +10,7 @@ import Button from "src/Components/Button/Button";
import { FiCopy } from "react-icons/fi"; import { FiCopy } from "react-icons/fi";
import useCopyToClipboard from "src/utils/hooks/useCopyToClipboard"; import useCopyToClipboard from "src/utils/hooks/useCopyToClipboard";
import { getPropertyFromUnknown, trimText, } from "src/utils/helperFunctions"; import { getPropertyFromUnknown, trimText, } from "src/utils/helperFunctions";
import { fetchIsLoggedIn, fetchLnurlAuth } from "src/api/auth";

View File

@@ -2,8 +2,9 @@ import { yupResolver } from "@hookform/resolvers/yup";
import { Controller, FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form"; import { Controller, FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form";
import Button from "src/Components/Button/Button"; import Button from "src/Components/Button/Button";
import DatePicker from "src/Components/Inputs/DatePicker/DatePicker"; import DatePicker from "src/Components/Inputs/DatePicker/DatePicker";
import FilesInput from "src/Components/Inputs/FilesInput/FilesInput";
import TagsInput from "src/Components/Inputs/TagsInput/TagsInput"; import TagsInput from "src/Components/Inputs/TagsInput/TagsInput";
import { Tag } from "src/graphql";
import { imageSchema } from "src/utils/validation";
import * as yup from "yup"; import * as yup from "yup";
import ContentEditor from "../ContentEditor/ContentEditor"; import ContentEditor from "../ContentEditor/ContentEditor";
@@ -31,29 +32,14 @@ const schema = yup.object({
.string() .string()
.required() .required()
.min(50, 'you have to write at least 10 words'), .min(50, 'you have to write at least 10 words'),
cover_image: yup cover_image: imageSchema,
.lazy((value: string | File[]) => {
switch (typeof value) {
case 'object':
return yup
.array()
.test("fileSize", "File Size is too large", (files) => (files as File[]).every(file => file.size <= 5242880))
.test("fileType", "Unsupported File Format, only png/jpg/jpeg images are allowed",
(files) => (files as File[]).every((file: File) =>
["image/jpeg", "image/png", "image/jpg"].includes(file.type)))
case 'string':
return yup.string().url();
default:
return yup.mixed()
}
})
}).required(); }).required();
interface IFormInputs { interface IFormInputs {
title: string title: string
deadline: Date deadline: Date
bounty_amount: number bounty_amount: number
tags: NestedValue<object[]> tags: NestedValue<Tag[]>
cover_image: NestedValue<File[]> | string cover_image: NestedValue<File[]> | string
body: string body: string
} }
@@ -86,7 +72,7 @@ export default function BountyForm() {
<div <div
className='bg-white shadow-lg rounded-8 overflow-hidden'> className='bg-white shadow-lg rounded-8 overflow-hidden'>
<div className="p-32"> <div className="p-32">
<Controller {/* <Controller
control={control} control={control}
name="cover_image" name="cover_image"
render={({ field: { onChange, value, onBlur } }) => ( render={({ field: { onChange, value, onBlur } }) => (
@@ -97,7 +83,7 @@ export default function BountyForm() {
uploadText='Add a cover image' uploadText='Add a cover image'
/> />
)} )}
/> /> */}
<p className='input-error'>{errors.cover_image?.message}</p> <p className='input-error'>{errors.cover_image?.message}</p>
@@ -155,10 +141,20 @@ export default function BountyForm() {
<p className="text-body5 mt-16"> <p className="text-body5 mt-16">
Tags Tags
</p> </p>
<Controller
control={control}
name="tags"
render={({ field: { onChange, value, onBlur } }) => (
<TagsInput <TagsInput
placeholder="Enter your tag and click enter. You can add multiple tags to your post" placeholder="Add up to 5 popular tags..."
classes={{ container: 'mt-8' }} classes={{ container: 'mt-16' }}
value={value}
onChange={onChange}
onBlur={onBlur}
/> />
)}
/>
{errors.tags && <p className="input-error"> {errors.tags && <p className="input-error">
{errors.tags.message} {errors.tags.message}
</p>} </p>}

View File

@@ -14,7 +14,6 @@ export default {
decorators: [WithModals, WrapForm<IStoryFormInputs>({ decorators: [WithModals, WrapForm<IStoryFormInputs>({
defaultValues: { defaultValues: {
tags: [], tags: [],
cover_image: [],
} }
})] })]
} as ComponentMeta<typeof DraftsContainer>; } as ComponentMeta<typeof DraftsContainer>;

View File

@@ -10,7 +10,7 @@ import { NotificationsService } from 'src/services';
import { getDateDifference } from 'src/utils/helperFunctions'; import { getDateDifference } from 'src/utils/helperFunctions';
import { useAppDispatch } from 'src/utils/hooks'; import { useAppDispatch } from 'src/utils/hooks';
import { useReduxEffect } from 'src/utils/hooks/useReduxEffect'; import { useReduxEffect } from 'src/utils/hooks/useReduxEffect';
import { IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage'; import { CreateStoryType, IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage';
interface Props { interface Props {
id?: string; id?: string;
@@ -28,7 +28,7 @@ export default function DraftsContainer({ id, type, onDraftLoad }: Props) {
const [deleteStory] = useDeleteStoryMutation({ const [deleteStory] = useDeleteStoryMutation({
refetchQueries: ['GetMyDrafts'] refetchQueries: ['GetMyDrafts']
}) })
const { setValue } = useFormContext<IStoryFormInputs>() const { setValue } = useFormContext<CreateStoryType>()
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
@@ -45,7 +45,7 @@ export default function DraftsContainer({ id, type, onDraftLoad }: Props) {
setValue('title', data.getPostById.title); setValue('title', data.getPostById.title);
setValue('tags', data.getPostById.tags); setValue('tags', data.getPostById.tags);
setValue('body', data.getPostById.body); setValue('body', data.getPostById.body);
setValue('cover_image', data.getPostById.cover_image ? [data.getPostById.cover_image] : []); setValue('cover_image', data.getPostById.cover_image ? { url: data.getPostById.cover_image, id: null, name: null } : null);
setValue('is_published', data.getPostById.is_published); setValue('is_published', data.getPostById.is_published);
} }

View File

@@ -1,8 +1,8 @@
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { Controller, FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form"; import { Controller, FormProvider, NestedValue, Resolver, SubmitHandler, useForm } from "react-hook-form";
import Button from "src/Components/Button/Button"; import Button from "src/Components/Button/Button";
import FilesInput from "src/Components/Inputs/FilesInput/FilesInput";
import TagsInput from "src/Components/Inputs/TagsInput/TagsInput"; import TagsInput from "src/Components/Inputs/TagsInput/TagsInput";
import { Tag } from "src/graphql";
import * as yup from "yup"; import * as yup from "yup";
import ContentEditor from "../ContentEditor/ContentEditor"; import ContentEditor from "../ContentEditor/ContentEditor";
@@ -29,7 +29,7 @@ const schema = yup.object({
interface IFormInputs { interface IFormInputs {
title: string title: string
tags: NestedValue<object[]> tags: NestedValue<Tag[]>
cover_image: NestedValue<File[]> | string cover_image: NestedValue<File[]> | string
body: string body: string
} }
@@ -60,7 +60,7 @@ export default function QuestionForm() {
<div <div
className='bg-white shadow-lg rounded-8 overflow-hidden'> className='bg-white shadow-lg rounded-8 overflow-hidden'>
<div className="p-32"> <div className="p-32">
<Controller {/* <Controller
control={control} control={control}
name="cover_image" name="cover_image"
render={({ field: { onChange, value, onBlur } }) => ( render={({ field: { onChange, value, onBlur } }) => (
@@ -71,7 +71,7 @@ export default function QuestionForm() {
uploadText='Add a cover image' uploadText='Add a cover image'
/> />
)} )}
/> /> */}
<p className='input-error'>{errors.cover_image?.message}</p> <p className='input-error'>{errors.cover_image?.message}</p>
@@ -95,9 +95,18 @@ export default function QuestionForm() {
<p className="text-body5 mt-16"> <p className="text-body5 mt-16">
Tags Tags
</p> </p>
<Controller
control={control}
name="tags"
render={({ field: { onChange, value, onBlur } }) => (
<TagsInput <TagsInput
placeholder="Enter your tag and click enter. You can add multiple tags to your post" placeholder="Add up to 5 popular tags..."
classes={{ container: 'mt-8' }} classes={{ container: 'mt-16' }}
value={value}
onChange={onChange}
onBlur={onBlur}
/>
)}
/> />
{errors.tags && <p className="input-error"> {errors.tags && <p className="input-error">
{errors.tags.message} {errors.tags.message}

View File

@@ -13,7 +13,6 @@ export default {
decorators: [WithModals, WrapForm<IStoryFormInputs>({ decorators: [WithModals, WrapForm<IStoryFormInputs>({
defaultValues: { defaultValues: {
tags: [], tags: [],
cover_image: [],
} }
})] })]
} as ComponentMeta<typeof StoryForm>; } as ComponentMeta<typeof StoryForm>;

View File

@@ -1,7 +1,6 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { Controller, useFormContext } from "react-hook-form"; import { Controller, useFormContext } from "react-hook-form";
import Button from "src/Components/Button/Button"; import Button from "src/Components/Button/Button";
import FilesInput from "src/Components/Inputs/FilesInput/FilesInput";
import TagsInput from "src/Components/Inputs/TagsInput/TagsInput"; import TagsInput from "src/Components/Inputs/TagsInput/TagsInput";
import ContentEditor from "../ContentEditor/ContentEditor"; import ContentEditor from "../ContentEditor/ContentEditor";
import { useCreateStoryMutation } from 'src/graphql' import { useCreateStoryMutation } from 'src/graphql'
@@ -13,7 +12,8 @@ import { createRoute } from 'src/utils/routing';
import PreviewPostCard from '../PreviewPostCard/PreviewPostCard' import PreviewPostCard from '../PreviewPostCard/PreviewPostCard'
import { StorageService } from 'src/services'; import { StorageService } from 'src/services';
import { useThrottledCallback } from '@react-hookz/web'; import { useThrottledCallback } from '@react-hookz/web';
import { CreateStoryType, IStoryFormInputs } from '../../CreateStoryPage/CreateStoryPage'; import { CreateStoryType } from '../../CreateStoryPage/CreateStoryPage';
import CoverImageInput from 'src/Components/Inputs/FilesInputs/CoverImageInput/CoverImageInput';
interface Props { interface Props {
isUpdating?: boolean; isUpdating?: boolean;
@@ -29,7 +29,7 @@ export default function StoryForm(props: Props) {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const { handleSubmit, control, register, trigger, getValues, watch, reset } = useFormContext<IStoryFormInputs>(); const { handleSubmit, control, register, trigger, getValues, watch, reset } = useFormContext<CreateStoryType>();
const [editMode, setEditMode] = useState(true) const [editMode, setEditMode] = useState(true)
@@ -80,7 +80,7 @@ export default function StoryForm(props: Props) {
refetchQueries: ['GetMyDrafts'] refetchQueries: ['GetMyDrafts']
}); });
const clickSubmit = (publish_now: boolean) => handleSubmit<IStoryFormInputs>(data => { const clickSubmit = (publish_now: boolean) => handleSubmit<CreateStoryType>(data => {
setLoading(true); setLoading(true);
createStory({ createStory({
variables: { variables: {
@@ -90,7 +90,7 @@ export default function StoryForm(props: Props) {
body: data.body, body: data.body,
tags: data.tags.map(t => t.title), tags: data.tags.map(t => t.title),
is_published: publish_now, is_published: publish_now,
cover_image: (data.cover_image[0] ?? null) as string | null, cover_image: data.cover_image,
}, },
} }
}) })
@@ -103,6 +103,8 @@ export default function StoryForm(props: Props) {
const { ref: registerTitleRef, ...titleRegisteration } = register('title'); const { ref: registerTitleRef, ...titleRegisteration } = register('title');
return ( return (
<> <>
<div id='preview-switch' className="flex gap-16"> <div id='preview-switch' className="flex gap-16">
@@ -117,19 +119,21 @@ export default function StoryForm(props: Props) {
<div <div
className='bg-white border-2 border-gray-200 rounded-16 overflow-hidden'> className='bg-white border-2 border-gray-200 rounded-16 overflow-hidden'>
<div className="p-16 md:p-24 lg:p-32"> <div className="p-16 md:p-24 lg:p-32">
<div className="w-full h-[120px] md:h-[240px] rounded-12 mb-16 overflow-hidden">
<Controller <Controller
control={control} control={control}
name="cover_image" name="cover_image"
render={({ field: { onChange, value, onBlur, ref } }) => ( render={({ field: { onChange, value, onBlur, ref } }) => <CoverImageInput
<FilesInput
ref={ref}
value={value} value={value}
onBlur={onBlur} onChange={e => {
onChange={onChange} onChange(e)
uploadText='Add a cover image' }}
// uploadText='Add a cover image'
/> />
)}
}
/> />
</div>
@@ -153,10 +157,20 @@ export default function StoryForm(props: Props) {
/> />
</div> </div>
<Controller
control={control}
name="tags"
render={({ field: { onChange, value, onBlur } }) => (
<TagsInput <TagsInput
placeholder="Add up to 5 popular tags..." placeholder="Add up to 5 popular tags..."
classes={{ container: 'mt-16' }} classes={{ container: 'mt-16' }}
value={value}
onChange={onChange}
onBlur={onBlur}
/> />
)}
/>
</div> </div>
<ContentEditor <ContentEditor
@@ -167,7 +181,7 @@ export default function StoryForm(props: Props) {
/> />
</div> </div>
</>} </>}
{!editMode && <PreviewPostCard post={{ ...getValues(), cover_image: getValues().cover_image[0] }} />} {!editMode && <PreviewPostCard post={{ ...getValues(), cover_image: getValues('cover_image.url') }} />}
<div className="flex gap-16 mt-32"> <div className="flex gap-16 mt-32">
<Button <Button
type='submit' type='submit'

View File

@@ -2,54 +2,42 @@
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { FormProvider, NestedValue, Resolver, useForm } from "react-hook-form"; import { FormProvider, NestedValue, Resolver, useForm } from "react-hook-form";
import { Post_Type } from "src/graphql"; import { CreateStoryMutationVariables, Post_Type } from "src/graphql";
import { StorageService } from "src/services"; import { StorageService } from "src/services";
import { useAppSelector } from "src/utils/hooks"; import { useAppSelector } from "src/utils/hooks";
import { Override } from "src/utils/interfaces"; import { Override } from "src/utils/interfaces";
import { imageSchema, tagSchema } from "src/utils/validation";
import * as yup from "yup"; import * as yup from "yup";
import DraftsContainer from "../Components/DraftsContainer/DraftsContainer"; import DraftsContainer from "../Components/DraftsContainer/DraftsContainer";
import ErrorsContainer from "../Components/ErrorsContainer/ErrorsContainer"; import ErrorsContainer from "../Components/ErrorsContainer/ErrorsContainer";
import StoryForm from "../Components/StoryForm/StoryForm"; import StoryForm from "../Components/StoryForm/StoryForm";
import styles from './styles.module.scss' import styles from './styles.module.scss'
const FileSchema = yup.lazy((value: string | File[]) => {
switch (typeof value) {
case 'object':
return yup.mixed()
.test("fileSize", "File Size is too large", file => file.size <= 5242880)
.test("fileType", "Unsupported File Format, only png/jpg/jpeg images are allowed",
(file: File) =>
["image/jpeg", "image/png", "image/jpg"].includes(file.type))
case 'string':
return yup.string().url();
default:
return yup.mixed()
}
})
const schema = yup.object({ const schema = yup.object({
title: yup.string().trim().required().min(10, 'Story title must be 2+ words').transform(v => v.replace(/(\r\n|\n|\r)/gm, "")), title: yup.string().trim().required().min(10, 'Story title must be 2+ words').transform(v => v.replace(/(\r\n|\n|\r)/gm, "")),
tags: yup.array().required().min(1, 'Add at least one tag'), tags: yup.array().of(tagSchema).required().min(1, 'Add at least one tag'),
body: yup.string().required().min(50, 'Post must contain at least 10+ words'), body: yup.string().required("Write some content in the post").min(50, 'Post must contain at least 10+ words'),
cover_image: yup.array().of(FileSchema as any) cover_image: imageSchema.nullable(true),
}).required(); }).required();
export interface IStoryFormInputs { type ApiStoryInput = NonNullable<CreateStoryMutationVariables['data']>;
id: number | null
title: string export type IStoryFormInputs = {
tags: NestedValue<{ title: string }[]> id: ApiStoryInput['id']
cover_image: NestedValue<File[]> | NestedValue<string[]> title: ApiStoryInput['title']
body: string body: ApiStoryInput['body']
is_published: boolean | null cover_image: NestedValue<NonNullable<ApiStoryInput['cover_image']>> | null
tags: NestedValue<ApiStoryInput['tags']>
is_published: ApiStoryInput['is_published']
} }
export type CreateStoryType = Override<IStoryFormInputs, { export type CreateStoryType = Override<IStoryFormInputs, {
cover_image: ApiStoryInput['cover_image'],
tags: { title: string }[] tags: { title: string }[]
cover_image: File[] | string[]
}> }>
const storageService = new StorageService<CreateStoryType>('story-edit'); const storageService = new StorageService<CreateStoryType>('story-edit');
@@ -62,13 +50,13 @@ export default function CreateStoryPage() {
story: state.staging.story || storageService.get() story: state.staging.story || storageService.get()
})) }))
const formMethods = useForm<IStoryFormInputs>({ const formMethods = useForm<CreateStoryType>({
resolver: yupResolver(schema) as Resolver<IStoryFormInputs>, resolver: yupResolver(schema) as Resolver<CreateStoryType>,
shouldFocusError: false, shouldFocusError: false,
defaultValues: { defaultValues: {
id: story?.id ?? null, id: story?.id ?? null,
title: story?.title ?? '', title: story?.title ?? '',
cover_image: story?.cover_image ?? [], cover_image: story?.cover_image,
tags: story?.tags ?? [], tags: story?.tags ?? [],
body: story?.body ?? '', body: story?.body ?? '',
is_published: story?.is_published ?? false, is_published: story?.is_published ?? false,

View File

@@ -31,7 +31,8 @@ export default function StoryPageContent({ story }: Props) {
<Card id="content" onlyMd className="relative max"> <Card id="content" onlyMd className="relative max">
{story.cover_image && {story.cover_image &&
<img src={story.cover_image} <img src={story.cover_image}
className='w-full object-cover rounded-12 md:rounded-16 mb-16' className='w-full h-[120px] md:h-[240px] object-cover rounded-12 mb-16'
// className='w-full object-cover rounded-12 md:rounded-16 mb-16'
alt="" />} alt="" />}
<div className="flex flex-col gap-24 relative"> <div className="flex flex-col gap-24 relative">
{curUser?.id === story.author.id && <Menu {curUser?.id === story.author.id && <Menu

View File

@@ -28,7 +28,7 @@ export const useUpdateStory = (story: Story) => {
const handleEdit = () => { const handleEdit = () => {
dispatch(stageStory({ dispatch(stageStory({
...story, ...story,
cover_image: story.cover_image ? [story.cover_image] : [] cover_image: story.cover_image ? { id: null, name: null, url: story.cover_image } : null,
})) }))
navigate("/blog/create-post?type=story") navigate("/blog/create-post?type=story")

View File

@@ -1,9 +1,8 @@
import { SubmitHandler, useForm } from "react-hook-form" import { Controller, SubmitHandler, useForm } from "react-hook-form"
import { useUpdateProfileAboutMutation, useMyProfileAboutQuery, UpdateProfileAboutMutationVariables, UserBasicInfoFragmentDoc } from "src/graphql"; import { useUpdateProfileAboutMutation, useMyProfileAboutQuery, UpdateProfileAboutMutationVariables, UserBasicInfoFragmentDoc } from "src/graphql";
import { NotificationsService } from "src/services/notifications.service"; import { NotificationsService } from "src/services/notifications.service";
import * as yup from "yup"; import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
import { useAppDispatch, usePrompt } from "src/utils/hooks"; import { useAppDispatch, usePrompt } from "src/utils/hooks";
import SaveChangesCard from "../SaveChangesCard/SaveChangesCard"; import SaveChangesCard from "../SaveChangesCard/SaveChangesCard";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -12,15 +11,18 @@ import NotFoundPage from "src/features/Shared/pages/NotFoundPage/NotFoundPage";
import { setUser } from "src/redux/features/user.slice"; import { setUser } from "src/redux/features/user.slice";
import UpdateProfileAboutTabSkeleton from "./BasicProfileInfoTab.Skeleton"; import UpdateProfileAboutTabSkeleton from "./BasicProfileInfoTab.Skeleton";
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import AvatarInput from "src/Components/Inputs/FilesInputs/AvatarInput/AvatarInput";
import { imageSchema } from "src/utils/validation";
interface Props { interface Props {
} }
type IFormInputs = NonNullable<UpdateProfileAboutMutationVariables['data']>; type IFormInputs = NonNullable<UpdateProfileAboutMutationVariables['data']>;
const schema: yup.SchemaOf<IFormInputs> = yup.object({ const schema: yup.SchemaOf<IFormInputs> = yup.object({
name: yup.string().trim().required().min(2), name: yup.string().trim().required().min(2),
avatar: yup.string().url().required(), avatar: imageSchema.required(),
bio: yup.string().ensure(), bio: yup.string().ensure(),
email: yup.string().email().ensure(), email: yup.string().email().ensure(),
github: yup.string().ensure(), github: yup.string().ensure(),
@@ -55,8 +57,10 @@ const schema: yup.SchemaOf<IFormInputs> = yup.object({
export default function BasicProfileInfoTab() { export default function BasicProfileInfoTab() {
const { register, formState: { errors, isDirty, }, handleSubmit, reset } = useForm<IFormInputs>({ const { register, formState: { errors, isDirty, }, handleSubmit, reset, control } = useForm<IFormInputs>({
defaultValues: {}, defaultValues: {
},
resolver: yupResolver(schema), resolver: yupResolver(schema),
mode: 'onBlur', mode: 'onBlur',
}); });
@@ -65,7 +69,7 @@ export default function BasicProfileInfoTab() {
const profileQuery = useMyProfileAboutQuery({ const profileQuery = useMyProfileAboutQuery({
onCompleted: data => { onCompleted: data => {
if (data.me) if (data.me)
reset(data.me) reset({ ...data.me, avatar: { url: data.me.avatar } })
} }
}) })
const [mutate, mutationStatus] = useUpdateProfileAboutMutation(); const [mutate, mutationStatus] = useUpdateProfileAboutMutation();
@@ -107,7 +111,7 @@ export default function BasicProfileInfoTab() {
onCompleted: ({ updateProfileDetails: data }) => { onCompleted: ({ updateProfileDetails: data }) => {
if (data) { if (data) {
dispatch(setUser(data)) dispatch(setUser(data))
reset(data); reset({ ...data, avatar: { url: data.avatar } });
apolloClient.writeFragment({ apolloClient.writeFragment({
id: `User:${data?.id}`, id: `User:${data?.id}`,
data, data,
@@ -123,12 +127,21 @@ export default function BasicProfileInfoTab() {
}) })
}; };
return ( return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-24"> <div className="grid grid-cols-1 md:grid-cols-3 gap-24">
<Card className="md:col-span-2" defaultPadding={false}> <Card className="md:col-span-2" defaultPadding={false}>
<div className="bg-gray-600 relative h-[160px] rounded-t-16"> <div className="bg-gray-600 relative h-[160px] rounded-t-16">
<div className="absolute left-24 bottom-0 translate-y-1/2"> <div className="absolute left-24 bottom-0 translate-y-1/2">
<Avatar src={profileQuery.data.me.avatar} width={120} /> <Controller
control={control}
name="avatar"
render={({ field: { onChange, value } }) => (
<AvatarInput value={value} onChange={onChange} width={120} />
)}
/>
</div> </div>
</div> </div>
<div className="p-16 md:p-24 mt-64"> <div className="p-16 md:p-24 mt-64">
@@ -148,29 +161,14 @@ export default function BasicProfileInfoTab() {
{errors.name && <p className="input-error"> {errors.name && <p className="input-error">
{errors.name.message} {errors.name.message}
</p>} </p>}
<p className="text-body5 mt-16 font-medium">
Avatar
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder='https://images.com/my-avatar.jpg'
{...register("avatar")}
/>
</div>
{errors.avatar && <p className="input-error">
{errors.avatar.message}
</p>}
<p className="text-body5 mt-16 font-medium"> <p className="text-body5 mt-16 font-medium">
Bio Bio
</p> </p>
<div className="input-wrapper mt-8 relative"> <div className="input-wrapper mt-8 relative">
<textarea <textarea
rows={3} rows={4}
className="input-text !p-20" className="input-text"
placeholder='Tell others a little bit about yourself' placeholder='Tell others a little bit about yourself'
{...register("bio")} {...register("bio")}
/> />

View File

@@ -2,23 +2,16 @@ import { motion } from 'framer-motion'
import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer' import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContainer/ModalsContainer'
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { Grid } from "react-loader-spinner"; import { Grid } from "react-loader-spinner";
import { CONSTS } from "src/utils";
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import Button from "src/Components/Button/Button"; import Button from "src/Components/Button/Button";
import { FiCopy } from "react-icons/fi"; import { FiCopy } from "react-icons/fi";
import useCopyToClipboard from "src/utils/hooks/useCopyToClipboard"; import useCopyToClipboard from "src/utils/hooks/useCopyToClipboard";
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { IoClose } from 'react-icons/io5'; import { IoClose } from 'react-icons/io5';
import { fetchLnurlAuth } from 'src/api/auth';
const fetchLnurlAuth = async () => {
const res = await fetch(CONSTS.apiEndpoint + '/get-login-url?action=link', {
credentials: 'include'
})
const data = await res.json()
return data;
}
const useLnurlQuery = () => { const useLnurlQuery = () => {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)

View File

@@ -141,6 +141,12 @@ export type Hackathon = {
website: Scalars['String']; website: Scalars['String'];
}; };
export type ImageInput = {
id: InputMaybe<Scalars['String']>;
name: InputMaybe<Scalars['String']>;
url: Scalars['String'];
};
export type LnurlDetails = { export type LnurlDetails = {
__typename?: 'LnurlDetails'; __typename?: 'LnurlDetails';
commentAllowed: Maybe<Scalars['Int']>; commentAllowed: Maybe<Scalars['Int']>;
@@ -1031,13 +1037,13 @@ export const OfficialTagsDocument = gql`
* }); * });
*/ */
export function useOfficialTagsQuery(baseOptions?: Apollo.QueryHookOptions<OfficialTagsQuery, OfficialTagsQueryVariables>) { export function useOfficialTagsQuery(baseOptions?: Apollo.QueryHookOptions<OfficialTagsQuery, OfficialTagsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<OfficialTagsQuery, OfficialTagsQueryVariables>(OfficialTagsDocument, options); return Apollo.useQuery<OfficialTagsQuery, OfficialTagsQueryVariables>(OfficialTagsDocument, options);
} }
export function useOfficialTagsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<OfficialTagsQuery, OfficialTagsQueryVariables>) { export function useOfficialTagsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<OfficialTagsQuery, OfficialTagsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<OfficialTagsQuery, OfficialTagsQueryVariables>(OfficialTagsDocument, options); return Apollo.useLazyQuery<OfficialTagsQuery, OfficialTagsQueryVariables>(OfficialTagsDocument, options);
} }
export type OfficialTagsQueryHookResult = ReturnType<typeof useOfficialTagsQuery>; export type OfficialTagsQueryHookResult = ReturnType<typeof useOfficialTagsQuery>;
export type OfficialTagsLazyQueryHookResult = ReturnType<typeof useOfficialTagsLazyQuery>; export type OfficialTagsLazyQueryHookResult = ReturnType<typeof useOfficialTagsLazyQuery>;
export type OfficialTagsQueryResult = Apollo.QueryResult<OfficialTagsQuery, OfficialTagsQueryVariables>; export type OfficialTagsQueryResult = Apollo.QueryResult<OfficialTagsQuery, OfficialTagsQueryVariables>;
@@ -1068,13 +1074,13 @@ export const NavCategoriesDocument = gql`
* }); * });
*/ */
export function useNavCategoriesQuery(baseOptions?: Apollo.QueryHookOptions<NavCategoriesQuery, NavCategoriesQueryVariables>) { export function useNavCategoriesQuery(baseOptions?: Apollo.QueryHookOptions<NavCategoriesQuery, NavCategoriesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<NavCategoriesQuery, NavCategoriesQueryVariables>(NavCategoriesDocument, options); return Apollo.useQuery<NavCategoriesQuery, NavCategoriesQueryVariables>(NavCategoriesDocument, options);
} }
export function useNavCategoriesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<NavCategoriesQuery, NavCategoriesQueryVariables>) { export function useNavCategoriesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<NavCategoriesQuery, NavCategoriesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<NavCategoriesQuery, NavCategoriesQueryVariables>(NavCategoriesDocument, options); return Apollo.useLazyQuery<NavCategoriesQuery, NavCategoriesQueryVariables>(NavCategoriesDocument, options);
} }
export type NavCategoriesQueryHookResult = ReturnType<typeof useNavCategoriesQuery>; export type NavCategoriesQueryHookResult = ReturnType<typeof useNavCategoriesQuery>;
export type NavCategoriesLazyQueryHookResult = ReturnType<typeof useNavCategoriesLazyQuery>; export type NavCategoriesLazyQueryHookResult = ReturnType<typeof useNavCategoriesLazyQuery>;
export type NavCategoriesQueryResult = Apollo.QueryResult<NavCategoriesQuery, NavCategoriesQueryVariables>; export type NavCategoriesQueryResult = Apollo.QueryResult<NavCategoriesQuery, NavCategoriesQueryVariables>;
@@ -1109,13 +1115,13 @@ export const SearchProjectsDocument = gql`
* }); * });
*/ */
export function useSearchProjectsQuery(baseOptions: Apollo.QueryHookOptions<SearchProjectsQuery, SearchProjectsQueryVariables>) { export function useSearchProjectsQuery(baseOptions: Apollo.QueryHookOptions<SearchProjectsQuery, SearchProjectsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<SearchProjectsQuery, SearchProjectsQueryVariables>(SearchProjectsDocument, options); return Apollo.useQuery<SearchProjectsQuery, SearchProjectsQueryVariables>(SearchProjectsDocument, options);
} }
export function useSearchProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchProjectsQuery, SearchProjectsQueryVariables>) { export function useSearchProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchProjectsQuery, SearchProjectsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<SearchProjectsQuery, SearchProjectsQueryVariables>(SearchProjectsDocument, options); return Apollo.useLazyQuery<SearchProjectsQuery, SearchProjectsQueryVariables>(SearchProjectsDocument, options);
} }
export type SearchProjectsQueryHookResult = ReturnType<typeof useSearchProjectsQuery>; export type SearchProjectsQueryHookResult = ReturnType<typeof useSearchProjectsQuery>;
export type SearchProjectsLazyQueryHookResult = ReturnType<typeof useSearchProjectsLazyQuery>; export type SearchProjectsLazyQueryHookResult = ReturnType<typeof useSearchProjectsLazyQuery>;
export type SearchProjectsQueryResult = Apollo.QueryResult<SearchProjectsQuery, SearchProjectsQueryVariables>; export type SearchProjectsQueryResult = Apollo.QueryResult<SearchProjectsQuery, SearchProjectsQueryVariables>;
@@ -1148,13 +1154,13 @@ export const MeDocument = gql`
* }); * });
*/ */
export function useMeQuery(baseOptions?: Apollo.QueryHookOptions<MeQuery, MeQueryVariables>) { export function useMeQuery(baseOptions?: Apollo.QueryHookOptions<MeQuery, MeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<MeQuery, MeQueryVariables>(MeDocument, options); return Apollo.useQuery<MeQuery, MeQueryVariables>(MeDocument, options);
} }
export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeQuery, MeQueryVariables>) { export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeQuery, MeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, options); return Apollo.useLazyQuery<MeQuery, MeQueryVariables>(MeDocument, options);
} }
export type MeQueryHookResult = ReturnType<typeof useMeQuery>; export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>; export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>; export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
@@ -1185,13 +1191,13 @@ export const DonationsStatsDocument = gql`
* }); * });
*/ */
export function useDonationsStatsQuery(baseOptions?: Apollo.QueryHookOptions<DonationsStatsQuery, DonationsStatsQueryVariables>) { export function useDonationsStatsQuery(baseOptions?: Apollo.QueryHookOptions<DonationsStatsQuery, DonationsStatsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<DonationsStatsQuery, DonationsStatsQueryVariables>(DonationsStatsDocument, options); return Apollo.useQuery<DonationsStatsQuery, DonationsStatsQueryVariables>(DonationsStatsDocument, options);
} }
export function useDonationsStatsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<DonationsStatsQuery, DonationsStatsQueryVariables>) { export function useDonationsStatsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<DonationsStatsQuery, DonationsStatsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<DonationsStatsQuery, DonationsStatsQueryVariables>(DonationsStatsDocument, options); return Apollo.useLazyQuery<DonationsStatsQuery, DonationsStatsQueryVariables>(DonationsStatsDocument, options);
} }
export type DonationsStatsQueryHookResult = ReturnType<typeof useDonationsStatsQuery>; export type DonationsStatsQueryHookResult = ReturnType<typeof useDonationsStatsQuery>;
export type DonationsStatsLazyQueryHookResult = ReturnType<typeof useDonationsStatsLazyQuery>; export type DonationsStatsLazyQueryHookResult = ReturnType<typeof useDonationsStatsLazyQuery>;
export type DonationsStatsQueryResult = Apollo.QueryResult<DonationsStatsQuery, DonationsStatsQueryVariables>; export type DonationsStatsQueryResult = Apollo.QueryResult<DonationsStatsQuery, DonationsStatsQueryVariables>;
@@ -1225,9 +1231,9 @@ export type DonateMutationFn = Apollo.MutationFunction<DonateMutation, DonateMut
* }); * });
*/ */
export function useDonateMutation(baseOptions?: Apollo.MutationHookOptions<DonateMutation, DonateMutationVariables>) { export function useDonateMutation(baseOptions?: Apollo.MutationHookOptions<DonateMutation, DonateMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useMutation<DonateMutation, DonateMutationVariables>(DonateDocument, options); return Apollo.useMutation<DonateMutation, DonateMutationVariables>(DonateDocument, options);
} }
export type DonateMutationHookResult = ReturnType<typeof useDonateMutation>; export type DonateMutationHookResult = ReturnType<typeof useDonateMutation>;
export type DonateMutationResult = Apollo.MutationResult<DonateMutation>; export type DonateMutationResult = Apollo.MutationResult<DonateMutation>;
export type DonateMutationOptions = Apollo.BaseMutationOptions<DonateMutation, DonateMutationVariables>; export type DonateMutationOptions = Apollo.BaseMutationOptions<DonateMutation, DonateMutationVariables>;
@@ -1261,9 +1267,9 @@ export type ConfirmDonationMutationFn = Apollo.MutationFunction<ConfirmDonationM
* }); * });
*/ */
export function useConfirmDonationMutation(baseOptions?: Apollo.MutationHookOptions<ConfirmDonationMutation, ConfirmDonationMutationVariables>) { export function useConfirmDonationMutation(baseOptions?: Apollo.MutationHookOptions<ConfirmDonationMutation, ConfirmDonationMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useMutation<ConfirmDonationMutation, ConfirmDonationMutationVariables>(ConfirmDonationDocument, options); return Apollo.useMutation<ConfirmDonationMutation, ConfirmDonationMutationVariables>(ConfirmDonationDocument, options);
} }
export type ConfirmDonationMutationHookResult = ReturnType<typeof useConfirmDonationMutation>; export type ConfirmDonationMutationHookResult = ReturnType<typeof useConfirmDonationMutation>;
export type ConfirmDonationMutationResult = Apollo.MutationResult<ConfirmDonationMutation>; export type ConfirmDonationMutationResult = Apollo.MutationResult<ConfirmDonationMutation>;
export type ConfirmDonationMutationOptions = Apollo.BaseMutationOptions<ConfirmDonationMutation, ConfirmDonationMutationVariables>; export type ConfirmDonationMutationOptions = Apollo.BaseMutationOptions<ConfirmDonationMutation, ConfirmDonationMutationVariables>;
@@ -1305,13 +1311,13 @@ export const GetHackathonsDocument = gql`
* }); * });
*/ */
export function useGetHackathonsQuery(baseOptions?: Apollo.QueryHookOptions<GetHackathonsQuery, GetHackathonsQueryVariables>) { export function useGetHackathonsQuery(baseOptions?: Apollo.QueryHookOptions<GetHackathonsQuery, GetHackathonsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<GetHackathonsQuery, GetHackathonsQueryVariables>(GetHackathonsDocument, options); return Apollo.useQuery<GetHackathonsQuery, GetHackathonsQueryVariables>(GetHackathonsDocument, options);
} }
export function useGetHackathonsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetHackathonsQuery, GetHackathonsQueryVariables>) { export function useGetHackathonsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetHackathonsQuery, GetHackathonsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<GetHackathonsQuery, GetHackathonsQueryVariables>(GetHackathonsDocument, options); return Apollo.useLazyQuery<GetHackathonsQuery, GetHackathonsQueryVariables>(GetHackathonsDocument, options);
} }
export type GetHackathonsQueryHookResult = ReturnType<typeof useGetHackathonsQuery>; export type GetHackathonsQueryHookResult = ReturnType<typeof useGetHackathonsQuery>;
export type GetHackathonsLazyQueryHookResult = ReturnType<typeof useGetHackathonsLazyQuery>; export type GetHackathonsLazyQueryHookResult = ReturnType<typeof useGetHackathonsLazyQuery>;
export type GetHackathonsQueryResult = Apollo.QueryResult<GetHackathonsQuery, GetHackathonsQueryVariables>; export type GetHackathonsQueryResult = Apollo.QueryResult<GetHackathonsQuery, GetHackathonsQueryVariables>;
@@ -1362,13 +1368,13 @@ export const TrendingPostsDocument = gql`
* }); * });
*/ */
export function useTrendingPostsQuery(baseOptions?: Apollo.QueryHookOptions<TrendingPostsQuery, TrendingPostsQueryVariables>) { export function useTrendingPostsQuery(baseOptions?: Apollo.QueryHookOptions<TrendingPostsQuery, TrendingPostsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<TrendingPostsQuery, TrendingPostsQueryVariables>(TrendingPostsDocument, options); return Apollo.useQuery<TrendingPostsQuery, TrendingPostsQueryVariables>(TrendingPostsDocument, options);
} }
export function useTrendingPostsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TrendingPostsQuery, TrendingPostsQueryVariables>) { export function useTrendingPostsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TrendingPostsQuery, TrendingPostsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<TrendingPostsQuery, TrendingPostsQueryVariables>(TrendingPostsDocument, options); return Apollo.useLazyQuery<TrendingPostsQuery, TrendingPostsQueryVariables>(TrendingPostsDocument, options);
} }
export type TrendingPostsQueryHookResult = ReturnType<typeof useTrendingPostsQuery>; export type TrendingPostsQueryHookResult = ReturnType<typeof useTrendingPostsQuery>;
export type TrendingPostsLazyQueryHookResult = ReturnType<typeof useTrendingPostsLazyQuery>; export type TrendingPostsLazyQueryHookResult = ReturnType<typeof useTrendingPostsLazyQuery>;
export type TrendingPostsQueryResult = Apollo.QueryResult<TrendingPostsQuery, TrendingPostsQueryVariables>; export type TrendingPostsQueryResult = Apollo.QueryResult<TrendingPostsQuery, TrendingPostsQueryVariables>;
@@ -1411,13 +1417,13 @@ export const GetMyDraftsDocument = gql`
* }); * });
*/ */
export function useGetMyDraftsQuery(baseOptions: Apollo.QueryHookOptions<GetMyDraftsQuery, GetMyDraftsQueryVariables>) { export function useGetMyDraftsQuery(baseOptions: Apollo.QueryHookOptions<GetMyDraftsQuery, GetMyDraftsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<GetMyDraftsQuery, GetMyDraftsQueryVariables>(GetMyDraftsDocument, options); return Apollo.useQuery<GetMyDraftsQuery, GetMyDraftsQueryVariables>(GetMyDraftsDocument, options);
} }
export function useGetMyDraftsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetMyDraftsQuery, GetMyDraftsQueryVariables>) { export function useGetMyDraftsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetMyDraftsQuery, GetMyDraftsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<GetMyDraftsQuery, GetMyDraftsQueryVariables>(GetMyDraftsDocument, options); return Apollo.useLazyQuery<GetMyDraftsQuery, GetMyDraftsQueryVariables>(GetMyDraftsDocument, options);
} }
export type GetMyDraftsQueryHookResult = ReturnType<typeof useGetMyDraftsQuery>; export type GetMyDraftsQueryHookResult = ReturnType<typeof useGetMyDraftsQuery>;
export type GetMyDraftsLazyQueryHookResult = ReturnType<typeof useGetMyDraftsLazyQuery>; export type GetMyDraftsLazyQueryHookResult = ReturnType<typeof useGetMyDraftsLazyQuery>;
export type GetMyDraftsQueryResult = Apollo.QueryResult<GetMyDraftsQuery, GetMyDraftsQueryVariables>; export type GetMyDraftsQueryResult = Apollo.QueryResult<GetMyDraftsQuery, GetMyDraftsQueryVariables>;
@@ -1459,9 +1465,9 @@ export type CreateStoryMutationFn = Apollo.MutationFunction<CreateStoryMutation,
* }); * });
*/ */
export function useCreateStoryMutation(baseOptions?: Apollo.MutationHookOptions<CreateStoryMutation, CreateStoryMutationVariables>) { export function useCreateStoryMutation(baseOptions?: Apollo.MutationHookOptions<CreateStoryMutation, CreateStoryMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useMutation<CreateStoryMutation, CreateStoryMutationVariables>(CreateStoryDocument, options); return Apollo.useMutation<CreateStoryMutation, CreateStoryMutationVariables>(CreateStoryDocument, options);
} }
export type CreateStoryMutationHookResult = ReturnType<typeof useCreateStoryMutation>; export type CreateStoryMutationHookResult = ReturnType<typeof useCreateStoryMutation>;
export type CreateStoryMutationResult = Apollo.MutationResult<CreateStoryMutation>; export type CreateStoryMutationResult = Apollo.MutationResult<CreateStoryMutation>;
export type CreateStoryMutationOptions = Apollo.BaseMutationOptions<CreateStoryMutation, CreateStoryMutationVariables>; export type CreateStoryMutationOptions = Apollo.BaseMutationOptions<CreateStoryMutation, CreateStoryMutationVariables>;
@@ -1492,9 +1498,9 @@ export type DeleteStoryMutationFn = Apollo.MutationFunction<DeleteStoryMutation,
* }); * });
*/ */
export function useDeleteStoryMutation(baseOptions?: Apollo.MutationHookOptions<DeleteStoryMutation, DeleteStoryMutationVariables>) { export function useDeleteStoryMutation(baseOptions?: Apollo.MutationHookOptions<DeleteStoryMutation, DeleteStoryMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useMutation<DeleteStoryMutation, DeleteStoryMutationVariables>(DeleteStoryDocument, options); return Apollo.useMutation<DeleteStoryMutation, DeleteStoryMutationVariables>(DeleteStoryDocument, options);
} }
export type DeleteStoryMutationHookResult = ReturnType<typeof useDeleteStoryMutation>; export type DeleteStoryMutationHookResult = ReturnType<typeof useDeleteStoryMutation>;
export type DeleteStoryMutationResult = Apollo.MutationResult<DeleteStoryMutation>; export type DeleteStoryMutationResult = Apollo.MutationResult<DeleteStoryMutation>;
export type DeleteStoryMutationOptions = Apollo.BaseMutationOptions<DeleteStoryMutation, DeleteStoryMutationVariables>; export type DeleteStoryMutationOptions = Apollo.BaseMutationOptions<DeleteStoryMutation, DeleteStoryMutationVariables>;
@@ -1524,13 +1530,13 @@ export const PopularTagsDocument = gql`
* }); * });
*/ */
export function usePopularTagsQuery(baseOptions?: Apollo.QueryHookOptions<PopularTagsQuery, PopularTagsQueryVariables>) { export function usePopularTagsQuery(baseOptions?: Apollo.QueryHookOptions<PopularTagsQuery, PopularTagsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<PopularTagsQuery, PopularTagsQueryVariables>(PopularTagsDocument, options); return Apollo.useQuery<PopularTagsQuery, PopularTagsQueryVariables>(PopularTagsDocument, options);
} }
export function usePopularTagsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PopularTagsQuery, PopularTagsQueryVariables>) { export function usePopularTagsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PopularTagsQuery, PopularTagsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<PopularTagsQuery, PopularTagsQueryVariables>(PopularTagsDocument, options); return Apollo.useLazyQuery<PopularTagsQuery, PopularTagsQueryVariables>(PopularTagsDocument, options);
} }
export type PopularTagsQueryHookResult = ReturnType<typeof usePopularTagsQuery>; export type PopularTagsQueryHookResult = ReturnType<typeof usePopularTagsQuery>;
export type PopularTagsLazyQueryHookResult = ReturnType<typeof usePopularTagsLazyQuery>; export type PopularTagsLazyQueryHookResult = ReturnType<typeof usePopularTagsLazyQuery>;
export type PopularTagsQueryResult = Apollo.QueryResult<PopularTagsQuery, PopularTagsQueryVariables>; export type PopularTagsQueryResult = Apollo.QueryResult<PopularTagsQuery, PopularTagsQueryVariables>;
@@ -1621,13 +1627,13 @@ export const FeedDocument = gql`
* }); * });
*/ */
export function useFeedQuery(baseOptions?: Apollo.QueryHookOptions<FeedQuery, FeedQueryVariables>) { export function useFeedQuery(baseOptions?: Apollo.QueryHookOptions<FeedQuery, FeedQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<FeedQuery, FeedQueryVariables>(FeedDocument, options); return Apollo.useQuery<FeedQuery, FeedQueryVariables>(FeedDocument, options);
} }
export function useFeedLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FeedQuery, FeedQueryVariables>) { export function useFeedLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FeedQuery, FeedQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<FeedQuery, FeedQueryVariables>(FeedDocument, options); return Apollo.useLazyQuery<FeedQuery, FeedQueryVariables>(FeedDocument, options);
} }
export type FeedQueryHookResult = ReturnType<typeof useFeedQuery>; export type FeedQueryHookResult = ReturnType<typeof useFeedQuery>;
export type FeedLazyQueryHookResult = ReturnType<typeof useFeedLazyQuery>; export type FeedLazyQueryHookResult = ReturnType<typeof useFeedLazyQuery>;
export type FeedQueryResult = Apollo.QueryResult<FeedQuery, FeedQueryVariables>; export type FeedQueryResult = Apollo.QueryResult<FeedQuery, FeedQueryVariables>;
@@ -1726,13 +1732,13 @@ export const PostDetailsDocument = gql`
* }); * });
*/ */
export function usePostDetailsQuery(baseOptions: Apollo.QueryHookOptions<PostDetailsQuery, PostDetailsQueryVariables>) { export function usePostDetailsQuery(baseOptions: Apollo.QueryHookOptions<PostDetailsQuery, PostDetailsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<PostDetailsQuery, PostDetailsQueryVariables>(PostDetailsDocument, options); return Apollo.useQuery<PostDetailsQuery, PostDetailsQueryVariables>(PostDetailsDocument, options);
} }
export function usePostDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PostDetailsQuery, PostDetailsQueryVariables>) { export function usePostDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PostDetailsQuery, PostDetailsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<PostDetailsQuery, PostDetailsQueryVariables>(PostDetailsDocument, options); return Apollo.useLazyQuery<PostDetailsQuery, PostDetailsQueryVariables>(PostDetailsDocument, options);
} }
export type PostDetailsQueryHookResult = ReturnType<typeof usePostDetailsQuery>; export type PostDetailsQueryHookResult = ReturnType<typeof usePostDetailsQuery>;
export type PostDetailsLazyQueryHookResult = ReturnType<typeof usePostDetailsLazyQuery>; export type PostDetailsLazyQueryHookResult = ReturnType<typeof usePostDetailsLazyQuery>;
export type PostDetailsQueryResult = Apollo.QueryResult<PostDetailsQuery, PostDetailsQueryVariables>; export type PostDetailsQueryResult = Apollo.QueryResult<PostDetailsQuery, PostDetailsQueryVariables>;
@@ -1761,13 +1767,13 @@ export const MyProfileAboutDocument = gql`
* }); * });
*/ */
export function useMyProfileAboutQuery(baseOptions?: Apollo.QueryHookOptions<MyProfileAboutQuery, MyProfileAboutQueryVariables>) { export function useMyProfileAboutQuery(baseOptions?: Apollo.QueryHookOptions<MyProfileAboutQuery, MyProfileAboutQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<MyProfileAboutQuery, MyProfileAboutQueryVariables>(MyProfileAboutDocument, options); return Apollo.useQuery<MyProfileAboutQuery, MyProfileAboutQueryVariables>(MyProfileAboutDocument, options);
} }
export function useMyProfileAboutLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MyProfileAboutQuery, MyProfileAboutQueryVariables>) { export function useMyProfileAboutLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MyProfileAboutQuery, MyProfileAboutQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<MyProfileAboutQuery, MyProfileAboutQueryVariables>(MyProfileAboutDocument, options); return Apollo.useLazyQuery<MyProfileAboutQuery, MyProfileAboutQueryVariables>(MyProfileAboutDocument, options);
} }
export type MyProfileAboutQueryHookResult = ReturnType<typeof useMyProfileAboutQuery>; export type MyProfileAboutQueryHookResult = ReturnType<typeof useMyProfileAboutQuery>;
export type MyProfileAboutLazyQueryHookResult = ReturnType<typeof useMyProfileAboutLazyQuery>; export type MyProfileAboutLazyQueryHookResult = ReturnType<typeof useMyProfileAboutLazyQuery>;
export type MyProfileAboutQueryResult = Apollo.QueryResult<MyProfileAboutQuery, MyProfileAboutQueryVariables>; export type MyProfileAboutQueryResult = Apollo.QueryResult<MyProfileAboutQuery, MyProfileAboutQueryVariables>;
@@ -1799,9 +1805,9 @@ export type UpdateProfileAboutMutationFn = Apollo.MutationFunction<UpdateProfile
* }); * });
*/ */
export function useUpdateProfileAboutMutation(baseOptions?: Apollo.MutationHookOptions<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>) { export function useUpdateProfileAboutMutation(baseOptions?: Apollo.MutationHookOptions<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useMutation<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>(UpdateProfileAboutDocument, options); return Apollo.useMutation<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>(UpdateProfileAboutDocument, options);
} }
export type UpdateProfileAboutMutationHookResult = ReturnType<typeof useUpdateProfileAboutMutation>; export type UpdateProfileAboutMutationHookResult = ReturnType<typeof useUpdateProfileAboutMutation>;
export type UpdateProfileAboutMutationResult = Apollo.MutationResult<UpdateProfileAboutMutation>; export type UpdateProfileAboutMutationResult = Apollo.MutationResult<UpdateProfileAboutMutation>;
export type UpdateProfileAboutMutationOptions = Apollo.BaseMutationOptions<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>; export type UpdateProfileAboutMutationOptions = Apollo.BaseMutationOptions<UpdateProfileAboutMutation, UpdateProfileAboutMutationVariables>;
@@ -1836,13 +1842,13 @@ export const MyProfilePreferencesDocument = gql`
* }); * });
*/ */
export function useMyProfilePreferencesQuery(baseOptions?: Apollo.QueryHookOptions<MyProfilePreferencesQuery, MyProfilePreferencesQueryVariables>) { export function useMyProfilePreferencesQuery(baseOptions?: Apollo.QueryHookOptions<MyProfilePreferencesQuery, MyProfilePreferencesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<MyProfilePreferencesQuery, MyProfilePreferencesQueryVariables>(MyProfilePreferencesDocument, options); return Apollo.useQuery<MyProfilePreferencesQuery, MyProfilePreferencesQueryVariables>(MyProfilePreferencesDocument, options);
} }
export function useMyProfilePreferencesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MyProfilePreferencesQuery, MyProfilePreferencesQueryVariables>) { export function useMyProfilePreferencesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MyProfilePreferencesQuery, MyProfilePreferencesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<MyProfilePreferencesQuery, MyProfilePreferencesQueryVariables>(MyProfilePreferencesDocument, options); return Apollo.useLazyQuery<MyProfilePreferencesQuery, MyProfilePreferencesQueryVariables>(MyProfilePreferencesDocument, options);
} }
export type MyProfilePreferencesQueryHookResult = ReturnType<typeof useMyProfilePreferencesQuery>; export type MyProfilePreferencesQueryHookResult = ReturnType<typeof useMyProfilePreferencesQuery>;
export type MyProfilePreferencesLazyQueryHookResult = ReturnType<typeof useMyProfilePreferencesLazyQuery>; export type MyProfilePreferencesLazyQueryHookResult = ReturnType<typeof useMyProfilePreferencesLazyQuery>;
export type MyProfilePreferencesQueryResult = Apollo.QueryResult<MyProfilePreferencesQuery, MyProfilePreferencesQueryVariables>; export type MyProfilePreferencesQueryResult = Apollo.QueryResult<MyProfilePreferencesQuery, MyProfilePreferencesQueryVariables>;
@@ -1879,9 +1885,9 @@ export type UpdateUserPreferencesMutationFn = Apollo.MutationFunction<UpdateUser
* }); * });
*/ */
export function useUpdateUserPreferencesMutation(baseOptions?: Apollo.MutationHookOptions<UpdateUserPreferencesMutation, UpdateUserPreferencesMutationVariables>) { export function useUpdateUserPreferencesMutation(baseOptions?: Apollo.MutationHookOptions<UpdateUserPreferencesMutation, UpdateUserPreferencesMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useMutation<UpdateUserPreferencesMutation, UpdateUserPreferencesMutationVariables>(UpdateUserPreferencesDocument, options); return Apollo.useMutation<UpdateUserPreferencesMutation, UpdateUserPreferencesMutationVariables>(UpdateUserPreferencesDocument, options);
} }
export type UpdateUserPreferencesMutationHookResult = ReturnType<typeof useUpdateUserPreferencesMutation>; export type UpdateUserPreferencesMutationHookResult = ReturnType<typeof useUpdateUserPreferencesMutation>;
export type UpdateUserPreferencesMutationResult = Apollo.MutationResult<UpdateUserPreferencesMutation>; export type UpdateUserPreferencesMutationResult = Apollo.MutationResult<UpdateUserPreferencesMutation>;
export type UpdateUserPreferencesMutationOptions = Apollo.BaseMutationOptions<UpdateUserPreferencesMutation, UpdateUserPreferencesMutationVariables>; export type UpdateUserPreferencesMutationOptions = Apollo.BaseMutationOptions<UpdateUserPreferencesMutation, UpdateUserPreferencesMutationVariables>;
@@ -1919,13 +1925,13 @@ export const MyProfileRolesSkillsDocument = gql`
* }); * });
*/ */
export function useMyProfileRolesSkillsQuery(baseOptions?: Apollo.QueryHookOptions<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>) { export function useMyProfileRolesSkillsQuery(baseOptions?: Apollo.QueryHookOptions<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>(MyProfileRolesSkillsDocument, options); return Apollo.useQuery<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>(MyProfileRolesSkillsDocument, options);
} }
export function useMyProfileRolesSkillsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>) { export function useMyProfileRolesSkillsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>(MyProfileRolesSkillsDocument, options); return Apollo.useLazyQuery<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>(MyProfileRolesSkillsDocument, options);
} }
export type MyProfileRolesSkillsQueryHookResult = ReturnType<typeof useMyProfileRolesSkillsQuery>; export type MyProfileRolesSkillsQueryHookResult = ReturnType<typeof useMyProfileRolesSkillsQuery>;
export type MyProfileRolesSkillsLazyQueryHookResult = ReturnType<typeof useMyProfileRolesSkillsLazyQuery>; export type MyProfileRolesSkillsLazyQueryHookResult = ReturnType<typeof useMyProfileRolesSkillsLazyQuery>;
export type MyProfileRolesSkillsQueryResult = Apollo.QueryResult<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>; export type MyProfileRolesSkillsQueryResult = Apollo.QueryResult<MyProfileRolesSkillsQuery, MyProfileRolesSkillsQueryVariables>;
@@ -1966,9 +1972,9 @@ export type UpdateUserRolesSkillsMutationFn = Apollo.MutationFunction<UpdateUser
* }); * });
*/ */
export function useUpdateUserRolesSkillsMutation(baseOptions?: Apollo.MutationHookOptions<UpdateUserRolesSkillsMutation, UpdateUserRolesSkillsMutationVariables>) { export function useUpdateUserRolesSkillsMutation(baseOptions?: Apollo.MutationHookOptions<UpdateUserRolesSkillsMutation, UpdateUserRolesSkillsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useMutation<UpdateUserRolesSkillsMutation, UpdateUserRolesSkillsMutationVariables>(UpdateUserRolesSkillsDocument, options); return Apollo.useMutation<UpdateUserRolesSkillsMutation, UpdateUserRolesSkillsMutationVariables>(UpdateUserRolesSkillsDocument, options);
} }
export type UpdateUserRolesSkillsMutationHookResult = ReturnType<typeof useUpdateUserRolesSkillsMutation>; export type UpdateUserRolesSkillsMutationHookResult = ReturnType<typeof useUpdateUserRolesSkillsMutation>;
export type UpdateUserRolesSkillsMutationResult = Apollo.MutationResult<UpdateUserRolesSkillsMutation>; export type UpdateUserRolesSkillsMutationResult = Apollo.MutationResult<UpdateUserRolesSkillsMutation>;
export type UpdateUserRolesSkillsMutationOptions = Apollo.BaseMutationOptions<UpdateUserRolesSkillsMutation, UpdateUserRolesSkillsMutationVariables>; export type UpdateUserRolesSkillsMutationOptions = Apollo.BaseMutationOptions<UpdateUserRolesSkillsMutation, UpdateUserRolesSkillsMutationVariables>;
@@ -2022,13 +2028,13 @@ ${UserRolesSkillsFragmentDoc}`;
* }); * });
*/ */
export function useProfileQuery(baseOptions: Apollo.QueryHookOptions<ProfileQuery, ProfileQueryVariables>) { export function useProfileQuery(baseOptions: Apollo.QueryHookOptions<ProfileQuery, ProfileQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<ProfileQuery, ProfileQueryVariables>(ProfileDocument, options); return Apollo.useQuery<ProfileQuery, ProfileQueryVariables>(ProfileDocument, options);
} }
export function useProfileLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProfileQuery, ProfileQueryVariables>) { export function useProfileLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProfileQuery, ProfileQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<ProfileQuery, ProfileQueryVariables>(ProfileDocument, options); return Apollo.useLazyQuery<ProfileQuery, ProfileQueryVariables>(ProfileDocument, options);
} }
export type ProfileQueryHookResult = ReturnType<typeof useProfileQuery>; export type ProfileQueryHookResult = ReturnType<typeof useProfileQuery>;
export type ProfileLazyQueryHookResult = ReturnType<typeof useProfileLazyQuery>; export type ProfileLazyQueryHookResult = ReturnType<typeof useProfileLazyQuery>;
export type ProfileQueryResult = Apollo.QueryResult<ProfileQuery, ProfileQueryVariables>; export type ProfileQueryResult = Apollo.QueryResult<ProfileQuery, ProfileQueryVariables>;
@@ -2070,13 +2076,13 @@ export const CategoryPageDocument = gql`
* }); * });
*/ */
export function useCategoryPageQuery(baseOptions: Apollo.QueryHookOptions<CategoryPageQuery, CategoryPageQueryVariables>) { export function useCategoryPageQuery(baseOptions: Apollo.QueryHookOptions<CategoryPageQuery, CategoryPageQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<CategoryPageQuery, CategoryPageQueryVariables>(CategoryPageDocument, options); return Apollo.useQuery<CategoryPageQuery, CategoryPageQueryVariables>(CategoryPageDocument, options);
} }
export function useCategoryPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<CategoryPageQuery, CategoryPageQueryVariables>) { export function useCategoryPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<CategoryPageQuery, CategoryPageQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<CategoryPageQuery, CategoryPageQueryVariables>(CategoryPageDocument, options); return Apollo.useLazyQuery<CategoryPageQuery, CategoryPageQueryVariables>(CategoryPageDocument, options);
} }
export type CategoryPageQueryHookResult = ReturnType<typeof useCategoryPageQuery>; export type CategoryPageQueryHookResult = ReturnType<typeof useCategoryPageQuery>;
export type CategoryPageLazyQueryHookResult = ReturnType<typeof useCategoryPageLazyQuery>; export type CategoryPageLazyQueryHookResult = ReturnType<typeof useCategoryPageLazyQuery>;
export type CategoryPageQueryResult = Apollo.QueryResult<CategoryPageQuery, CategoryPageQueryVariables>; export type CategoryPageQueryResult = Apollo.QueryResult<CategoryPageQuery, CategoryPageQueryVariables>;
@@ -2106,13 +2112,13 @@ export const AllCategoriesDocument = gql`
* }); * });
*/ */
export function useAllCategoriesQuery(baseOptions?: Apollo.QueryHookOptions<AllCategoriesQuery, AllCategoriesQueryVariables>) { export function useAllCategoriesQuery(baseOptions?: Apollo.QueryHookOptions<AllCategoriesQuery, AllCategoriesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<AllCategoriesQuery, AllCategoriesQueryVariables>(AllCategoriesDocument, options); return Apollo.useQuery<AllCategoriesQuery, AllCategoriesQueryVariables>(AllCategoriesDocument, options);
} }
export function useAllCategoriesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AllCategoriesQuery, AllCategoriesQueryVariables>) { export function useAllCategoriesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AllCategoriesQuery, AllCategoriesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<AllCategoriesQuery, AllCategoriesQueryVariables>(AllCategoriesDocument, options); return Apollo.useLazyQuery<AllCategoriesQuery, AllCategoriesQueryVariables>(AllCategoriesDocument, options);
} }
export type AllCategoriesQueryHookResult = ReturnType<typeof useAllCategoriesQuery>; export type AllCategoriesQueryHookResult = ReturnType<typeof useAllCategoriesQuery>;
export type AllCategoriesLazyQueryHookResult = ReturnType<typeof useAllCategoriesLazyQuery>; export type AllCategoriesLazyQueryHookResult = ReturnType<typeof useAllCategoriesLazyQuery>;
export type AllCategoriesQueryResult = Apollo.QueryResult<AllCategoriesQuery, AllCategoriesQueryVariables>; export type AllCategoriesQueryResult = Apollo.QueryResult<AllCategoriesQuery, AllCategoriesQueryVariables>;
@@ -2171,13 +2177,13 @@ export const ExploreProjectsDocument = gql`
* }); * });
*/ */
export function useExploreProjectsQuery(baseOptions?: Apollo.QueryHookOptions<ExploreProjectsQuery, ExploreProjectsQueryVariables>) { export function useExploreProjectsQuery(baseOptions?: Apollo.QueryHookOptions<ExploreProjectsQuery, ExploreProjectsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<ExploreProjectsQuery, ExploreProjectsQueryVariables>(ExploreProjectsDocument, options); return Apollo.useQuery<ExploreProjectsQuery, ExploreProjectsQueryVariables>(ExploreProjectsDocument, options);
} }
export function useExploreProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExploreProjectsQuery, ExploreProjectsQueryVariables>) { export function useExploreProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExploreProjectsQuery, ExploreProjectsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<ExploreProjectsQuery, ExploreProjectsQueryVariables>(ExploreProjectsDocument, options); return Apollo.useLazyQuery<ExploreProjectsQuery, ExploreProjectsQueryVariables>(ExploreProjectsDocument, options);
} }
export type ExploreProjectsQueryHookResult = ReturnType<typeof useExploreProjectsQuery>; export type ExploreProjectsQueryHookResult = ReturnType<typeof useExploreProjectsQuery>;
export type ExploreProjectsLazyQueryHookResult = ReturnType<typeof useExploreProjectsLazyQuery>; export type ExploreProjectsLazyQueryHookResult = ReturnType<typeof useExploreProjectsLazyQuery>;
export type ExploreProjectsQueryResult = Apollo.QueryResult<ExploreProjectsQuery, ExploreProjectsQueryVariables>; export type ExploreProjectsQueryResult = Apollo.QueryResult<ExploreProjectsQuery, ExploreProjectsQueryVariables>;
@@ -2212,13 +2218,13 @@ export const HottestProjectsDocument = gql`
* }); * });
*/ */
export function useHottestProjectsQuery(baseOptions?: Apollo.QueryHookOptions<HottestProjectsQuery, HottestProjectsQueryVariables>) { export function useHottestProjectsQuery(baseOptions?: Apollo.QueryHookOptions<HottestProjectsQuery, HottestProjectsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<HottestProjectsQuery, HottestProjectsQueryVariables>(HottestProjectsDocument, options); return Apollo.useQuery<HottestProjectsQuery, HottestProjectsQueryVariables>(HottestProjectsDocument, options);
} }
export function useHottestProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<HottestProjectsQuery, HottestProjectsQueryVariables>) { export function useHottestProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<HottestProjectsQuery, HottestProjectsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<HottestProjectsQuery, HottestProjectsQueryVariables>(HottestProjectsDocument, options); return Apollo.useLazyQuery<HottestProjectsQuery, HottestProjectsQueryVariables>(HottestProjectsDocument, options);
} }
export type HottestProjectsQueryHookResult = ReturnType<typeof useHottestProjectsQuery>; export type HottestProjectsQueryHookResult = ReturnType<typeof useHottestProjectsQuery>;
export type HottestProjectsLazyQueryHookResult = ReturnType<typeof useHottestProjectsLazyQuery>; export type HottestProjectsLazyQueryHookResult = ReturnType<typeof useHottestProjectsLazyQuery>;
export type HottestProjectsQueryResult = Apollo.QueryResult<HottestProjectsQuery, HottestProjectsQueryVariables>; export type HottestProjectsQueryResult = Apollo.QueryResult<HottestProjectsQuery, HottestProjectsQueryVariables>;
@@ -2270,13 +2276,13 @@ export const ProjectDetailsDocument = gql`
* }); * });
*/ */
export function useProjectDetailsQuery(baseOptions: Apollo.QueryHookOptions<ProjectDetailsQuery, ProjectDetailsQueryVariables>) { export function useProjectDetailsQuery(baseOptions: Apollo.QueryHookOptions<ProjectDetailsQuery, ProjectDetailsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<ProjectDetailsQuery, ProjectDetailsQueryVariables>(ProjectDetailsDocument, options); return Apollo.useQuery<ProjectDetailsQuery, ProjectDetailsQueryVariables>(ProjectDetailsDocument, options);
} }
export function useProjectDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProjectDetailsQuery, ProjectDetailsQueryVariables>) { export function useProjectDetailsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProjectDetailsQuery, ProjectDetailsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<ProjectDetailsQuery, ProjectDetailsQueryVariables>(ProjectDetailsDocument, options); return Apollo.useLazyQuery<ProjectDetailsQuery, ProjectDetailsQueryVariables>(ProjectDetailsDocument, options);
} }
export type ProjectDetailsQueryHookResult = ReturnType<typeof useProjectDetailsQuery>; export type ProjectDetailsQueryHookResult = ReturnType<typeof useProjectDetailsQuery>;
export type ProjectDetailsLazyQueryHookResult = ReturnType<typeof useProjectDetailsLazyQuery>; export type ProjectDetailsLazyQueryHookResult = ReturnType<typeof useProjectDetailsLazyQuery>;
export type ProjectDetailsQueryResult = Apollo.QueryResult<ProjectDetailsQuery, ProjectDetailsQueryVariables>; export type ProjectDetailsQueryResult = Apollo.QueryResult<ProjectDetailsQuery, ProjectDetailsQueryVariables>;
@@ -2306,13 +2312,13 @@ export const GetAllRolesDocument = gql`
* }); * });
*/ */
export function useGetAllRolesQuery(baseOptions?: Apollo.QueryHookOptions<GetAllRolesQuery, GetAllRolesQueryVariables>) { export function useGetAllRolesQuery(baseOptions?: Apollo.QueryHookOptions<GetAllRolesQuery, GetAllRolesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<GetAllRolesQuery, GetAllRolesQueryVariables>(GetAllRolesDocument, options); return Apollo.useQuery<GetAllRolesQuery, GetAllRolesQueryVariables>(GetAllRolesDocument, options);
} }
export function useGetAllRolesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetAllRolesQuery, GetAllRolesQueryVariables>) { export function useGetAllRolesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetAllRolesQuery, GetAllRolesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<GetAllRolesQuery, GetAllRolesQueryVariables>(GetAllRolesDocument, options); return Apollo.useLazyQuery<GetAllRolesQuery, GetAllRolesQueryVariables>(GetAllRolesDocument, options);
} }
export type GetAllRolesQueryHookResult = ReturnType<typeof useGetAllRolesQuery>; export type GetAllRolesQueryHookResult = ReturnType<typeof useGetAllRolesQuery>;
export type GetAllRolesLazyQueryHookResult = ReturnType<typeof useGetAllRolesLazyQuery>; export type GetAllRolesLazyQueryHookResult = ReturnType<typeof useGetAllRolesLazyQuery>;
export type GetAllRolesQueryResult = Apollo.QueryResult<GetAllRolesQuery, GetAllRolesQueryVariables>; export type GetAllRolesQueryResult = Apollo.QueryResult<GetAllRolesQuery, GetAllRolesQueryVariables>;
@@ -2376,13 +2382,13 @@ export const GetMakersInTournamentDocument = gql`
* }); * });
*/ */
export function useGetMakersInTournamentQuery(baseOptions: Apollo.QueryHookOptions<GetMakersInTournamentQuery, GetMakersInTournamentQueryVariables>) { export function useGetMakersInTournamentQuery(baseOptions: Apollo.QueryHookOptions<GetMakersInTournamentQuery, GetMakersInTournamentQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<GetMakersInTournamentQuery, GetMakersInTournamentQueryVariables>(GetMakersInTournamentDocument, options); return Apollo.useQuery<GetMakersInTournamentQuery, GetMakersInTournamentQueryVariables>(GetMakersInTournamentDocument, options);
} }
export function useGetMakersInTournamentLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetMakersInTournamentQuery, GetMakersInTournamentQueryVariables>) { export function useGetMakersInTournamentLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetMakersInTournamentQuery, GetMakersInTournamentQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<GetMakersInTournamentQuery, GetMakersInTournamentQueryVariables>(GetMakersInTournamentDocument, options); return Apollo.useLazyQuery<GetMakersInTournamentQuery, GetMakersInTournamentQueryVariables>(GetMakersInTournamentDocument, options);
} }
export type GetMakersInTournamentQueryHookResult = ReturnType<typeof useGetMakersInTournamentQuery>; export type GetMakersInTournamentQueryHookResult = ReturnType<typeof useGetMakersInTournamentQuery>;
export type GetMakersInTournamentLazyQueryHookResult = ReturnType<typeof useGetMakersInTournamentLazyQuery>; export type GetMakersInTournamentLazyQueryHookResult = ReturnType<typeof useGetMakersInTournamentLazyQuery>;
export type GetMakersInTournamentQueryResult = Apollo.QueryResult<GetMakersInTournamentQuery, GetMakersInTournamentQueryVariables>; export type GetMakersInTournamentQueryResult = Apollo.QueryResult<GetMakersInTournamentQuery, GetMakersInTournamentQueryVariables>;
@@ -2439,13 +2445,13 @@ export const GetProjectsInTournamentDocument = gql`
* }); * });
*/ */
export function useGetProjectsInTournamentQuery(baseOptions: Apollo.QueryHookOptions<GetProjectsInTournamentQuery, GetProjectsInTournamentQueryVariables>) { export function useGetProjectsInTournamentQuery(baseOptions: Apollo.QueryHookOptions<GetProjectsInTournamentQuery, GetProjectsInTournamentQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<GetProjectsInTournamentQuery, GetProjectsInTournamentQueryVariables>(GetProjectsInTournamentDocument, options); return Apollo.useQuery<GetProjectsInTournamentQuery, GetProjectsInTournamentQueryVariables>(GetProjectsInTournamentDocument, options);
} }
export function useGetProjectsInTournamentLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetProjectsInTournamentQuery, GetProjectsInTournamentQueryVariables>) { export function useGetProjectsInTournamentLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetProjectsInTournamentQuery, GetProjectsInTournamentQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<GetProjectsInTournamentQuery, GetProjectsInTournamentQueryVariables>(GetProjectsInTournamentDocument, options); return Apollo.useLazyQuery<GetProjectsInTournamentQuery, GetProjectsInTournamentQueryVariables>(GetProjectsInTournamentDocument, options);
} }
export type GetProjectsInTournamentQueryHookResult = ReturnType<typeof useGetProjectsInTournamentQuery>; export type GetProjectsInTournamentQueryHookResult = ReturnType<typeof useGetProjectsInTournamentQuery>;
export type GetProjectsInTournamentLazyQueryHookResult = ReturnType<typeof useGetProjectsInTournamentLazyQuery>; export type GetProjectsInTournamentLazyQueryHookResult = ReturnType<typeof useGetProjectsInTournamentLazyQuery>;
export type GetProjectsInTournamentQueryResult = Apollo.QueryResult<GetProjectsInTournamentQuery, GetProjectsInTournamentQueryVariables>; export type GetProjectsInTournamentQueryResult = Apollo.QueryResult<GetProjectsInTournamentQuery, GetProjectsInTournamentQueryVariables>;
@@ -2479,9 +2485,9 @@ export type UpdateTournamentRegistrationMutationFn = Apollo.MutationFunction<Upd
* }); * });
*/ */
export function useUpdateTournamentRegistrationMutation(baseOptions?: Apollo.MutationHookOptions<UpdateTournamentRegistrationMutation, UpdateTournamentRegistrationMutationVariables>) { export function useUpdateTournamentRegistrationMutation(baseOptions?: Apollo.MutationHookOptions<UpdateTournamentRegistrationMutation, UpdateTournamentRegistrationMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useMutation<UpdateTournamentRegistrationMutation, UpdateTournamentRegistrationMutationVariables>(UpdateTournamentRegistrationDocument, options); return Apollo.useMutation<UpdateTournamentRegistrationMutation, UpdateTournamentRegistrationMutationVariables>(UpdateTournamentRegistrationDocument, options);
} }
export type UpdateTournamentRegistrationMutationHookResult = ReturnType<typeof useUpdateTournamentRegistrationMutation>; export type UpdateTournamentRegistrationMutationHookResult = ReturnType<typeof useUpdateTournamentRegistrationMutation>;
export type UpdateTournamentRegistrationMutationResult = Apollo.MutationResult<UpdateTournamentRegistrationMutation>; export type UpdateTournamentRegistrationMutationResult = Apollo.MutationResult<UpdateTournamentRegistrationMutation>;
export type UpdateTournamentRegistrationMutationOptions = Apollo.BaseMutationOptions<UpdateTournamentRegistrationMutation, UpdateTournamentRegistrationMutationVariables>; export type UpdateTournamentRegistrationMutationOptions = Apollo.BaseMutationOptions<UpdateTournamentRegistrationMutation, UpdateTournamentRegistrationMutationVariables>;
@@ -2514,9 +2520,9 @@ export type RegisterInTournamentMutationFn = Apollo.MutationFunction<RegisterInT
* }); * });
*/ */
export function useRegisterInTournamentMutation(baseOptions?: Apollo.MutationHookOptions<RegisterInTournamentMutation, RegisterInTournamentMutationVariables>) { export function useRegisterInTournamentMutation(baseOptions?: Apollo.MutationHookOptions<RegisterInTournamentMutation, RegisterInTournamentMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useMutation<RegisterInTournamentMutation, RegisterInTournamentMutationVariables>(RegisterInTournamentDocument, options); return Apollo.useMutation<RegisterInTournamentMutation, RegisterInTournamentMutationVariables>(RegisterInTournamentDocument, options);
} }
export type RegisterInTournamentMutationHookResult = ReturnType<typeof useRegisterInTournamentMutation>; export type RegisterInTournamentMutationHookResult = ReturnType<typeof useRegisterInTournamentMutation>;
export type RegisterInTournamentMutationResult = Apollo.MutationResult<RegisterInTournamentMutation>; export type RegisterInTournamentMutationResult = Apollo.MutationResult<RegisterInTournamentMutation>;
export type RegisterInTournamentMutationOptions = Apollo.BaseMutationOptions<RegisterInTournamentMutation, RegisterInTournamentMutationVariables>; export type RegisterInTournamentMutationOptions = Apollo.BaseMutationOptions<RegisterInTournamentMutation, RegisterInTournamentMutationVariables>;
@@ -2556,13 +2562,13 @@ export const MeTournamentDocument = gql`
* }); * });
*/ */
export function useMeTournamentQuery(baseOptions: Apollo.QueryHookOptions<MeTournamentQuery, MeTournamentQueryVariables>) { export function useMeTournamentQuery(baseOptions: Apollo.QueryHookOptions<MeTournamentQuery, MeTournamentQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<MeTournamentQuery, MeTournamentQueryVariables>(MeTournamentDocument, options); return Apollo.useQuery<MeTournamentQuery, MeTournamentQueryVariables>(MeTournamentDocument, options);
} }
export function useMeTournamentLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeTournamentQuery, MeTournamentQueryVariables>) { export function useMeTournamentLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeTournamentQuery, MeTournamentQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<MeTournamentQuery, MeTournamentQueryVariables>(MeTournamentDocument, options); return Apollo.useLazyQuery<MeTournamentQuery, MeTournamentQueryVariables>(MeTournamentDocument, options);
} }
export type MeTournamentQueryHookResult = ReturnType<typeof useMeTournamentQuery>; export type MeTournamentQueryHookResult = ReturnType<typeof useMeTournamentQuery>;
export type MeTournamentLazyQueryHookResult = ReturnType<typeof useMeTournamentLazyQuery>; export type MeTournamentLazyQueryHookResult = ReturnType<typeof useMeTournamentLazyQuery>;
export type MeTournamentQueryResult = Apollo.QueryResult<MeTournamentQuery, MeTournamentQueryVariables>; export type MeTournamentQueryResult = Apollo.QueryResult<MeTournamentQuery, MeTournamentQueryVariables>;
@@ -2650,13 +2656,13 @@ export const GetTournamentByIdDocument = gql`
* }); * });
*/ */
export function useGetTournamentByIdQuery(baseOptions: Apollo.QueryHookOptions<GetTournamentByIdQuery, GetTournamentByIdQueryVariables>) { export function useGetTournamentByIdQuery(baseOptions: Apollo.QueryHookOptions<GetTournamentByIdQuery, GetTournamentByIdQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useQuery<GetTournamentByIdQuery, GetTournamentByIdQueryVariables>(GetTournamentByIdDocument, options); return Apollo.useQuery<GetTournamentByIdQuery, GetTournamentByIdQueryVariables>(GetTournamentByIdDocument, options);
} }
export function useGetTournamentByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetTournamentByIdQuery, GetTournamentByIdQueryVariables>) { export function useGetTournamentByIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetTournamentByIdQuery, GetTournamentByIdQueryVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useLazyQuery<GetTournamentByIdQuery, GetTournamentByIdQueryVariables>(GetTournamentByIdDocument, options); return Apollo.useLazyQuery<GetTournamentByIdQuery, GetTournamentByIdQueryVariables>(GetTournamentByIdDocument, options);
} }
export type GetTournamentByIdQueryHookResult = ReturnType<typeof useGetTournamentByIdQuery>; export type GetTournamentByIdQueryHookResult = ReturnType<typeof useGetTournamentByIdQuery>;
export type GetTournamentByIdLazyQueryHookResult = ReturnType<typeof useGetTournamentByIdLazyQuery>; export type GetTournamentByIdLazyQueryHookResult = ReturnType<typeof useGetTournamentByIdLazyQuery>;
export type GetTournamentByIdQueryResult = Apollo.QueryResult<GetTournamentByIdQuery, GetTournamentByIdQueryVariables>; export type GetTournamentByIdQueryResult = Apollo.QueryResult<GetTournamentByIdQuery, GetTournamentByIdQueryVariables>;
@@ -2695,9 +2701,9 @@ export type VoteMutationFn = Apollo.MutationFunction<VoteMutation, VoteMutationV
* }); * });
*/ */
export function useVoteMutation(baseOptions?: Apollo.MutationHookOptions<VoteMutation, VoteMutationVariables>) { export function useVoteMutation(baseOptions?: Apollo.MutationHookOptions<VoteMutation, VoteMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useMutation<VoteMutation, VoteMutationVariables>(VoteDocument, options); return Apollo.useMutation<VoteMutation, VoteMutationVariables>(VoteDocument, options);
} }
export type VoteMutationHookResult = ReturnType<typeof useVoteMutation>; export type VoteMutationHookResult = ReturnType<typeof useVoteMutation>;
export type VoteMutationResult = Apollo.MutationResult<VoteMutation>; export type VoteMutationResult = Apollo.MutationResult<VoteMutation>;
export type VoteMutationOptions = Apollo.BaseMutationOptions<VoteMutation, VoteMutationVariables>; export type VoteMutationOptions = Apollo.BaseMutationOptions<VoteMutation, VoteMutationVariables>;
@@ -2735,9 +2741,9 @@ export type ConfirmVoteMutationFn = Apollo.MutationFunction<ConfirmVoteMutation,
* }); * });
*/ */
export function useConfirmVoteMutation(baseOptions?: Apollo.MutationHookOptions<ConfirmVoteMutation, ConfirmVoteMutationVariables>) { export function useConfirmVoteMutation(baseOptions?: Apollo.MutationHookOptions<ConfirmVoteMutation, ConfirmVoteMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} const options = { ...defaultOptions, ...baseOptions }
return Apollo.useMutation<ConfirmVoteMutation, ConfirmVoteMutationVariables>(ConfirmVoteDocument, options); return Apollo.useMutation<ConfirmVoteMutation, ConfirmVoteMutationVariables>(ConfirmVoteDocument, options);
} }
export type ConfirmVoteMutationHookResult = ReturnType<typeof useConfirmVoteMutation>; export type ConfirmVoteMutationHookResult = ReturnType<typeof useConfirmVoteMutation>;
export type ConfirmVoteMutationResult = Apollo.MutationResult<ConfirmVoteMutation>; export type ConfirmVoteMutationResult = Apollo.MutationResult<ConfirmVoteMutation>;
export type ConfirmVoteMutationOptions = Apollo.BaseMutationOptions<ConfirmVoteMutation, ConfirmVoteMutationVariables>; export type ConfirmVoteMutationOptions = Apollo.BaseMutationOptions<ConfirmVoteMutation, ConfirmVoteMutationVariables>;

View File

@@ -2,9 +2,9 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Login_ScanningWalletCard, Login_ExternalWalletCard, Login_NativeWalletCard, Login_SuccessCard } from "src/Components/Modals/Login"; import { Login_ScanningWalletCard, Login_ExternalWalletCard, Login_NativeWalletCard, Login_SuccessCard } from "src/Components/Modals/Login";
import { ProjectDetailsCard } from "src/features/Projects/pages/ProjectPage/ProjectDetailsCard"; import { ProjectDetailsCard } from "src/features/Projects/pages/ProjectPage/ProjectDetailsCard";
import VoteCard from "src/features/Projects/pages/ProjectPage/VoteCard/VoteCard"; import VoteCard from "src/features/Projects/pages/ProjectPage/VoteCard/VoteCard";
import { InsertImageModal } from 'src/Components/Inputs/TextEditor/InsertImageModal'
import { InsertVideoModal } from 'src/Components/Inputs/TextEditor/InsertVideoModal' import { InsertVideoModal } from 'src/Components/Inputs/TextEditor/InsertVideoModal'
import { InsertLinkModal } from 'src/Components/Inputs/TextEditor/InsertLinkModal' import { InsertLinkModal } from 'src/Components/Inputs/TextEditor/InsertLinkModal'
import { Claim_FundWithdrawCard, Claim_CopySignatureCard, Claim_GenerateSignatureCard, Claim_SubmittedCard } from "src/features/Projects/pages/ProjectPage/ClaimProject"; import { Claim_FundWithdrawCard, Claim_CopySignatureCard, Claim_GenerateSignatureCard, Claim_SubmittedCard } from "src/features/Projects/pages/ProjectPage/ClaimProject";
import { ModalCard } from "src/Components/Modals/ModalsContainer/ModalsContainer"; import { ModalCard } from "src/Components/Modals/ModalsContainer/ModalsContainer";
import { ConfirmModal } from "src/Components/Modals/ConfirmModal"; import { ConfirmModal } from "src/Components/Modals/ConfirmModal";
@@ -18,6 +18,7 @@ import { ConnectToMakerModal } from "src/features/Tournaments/pages/MakersPage/C
import { RegistrationModals } from "src/features/Tournaments/pages/OverviewPage/RegisterationModals"; import { RegistrationModals } from "src/features/Tournaments/pages/OverviewPage/RegisterationModals";
import { InsertImageModal } from "src/Components/Modals/InsertImageModal";
export enum Direction { export enum Direction {
START, START,

View File

@@ -54,6 +54,8 @@
img { img {
border-radius: 16px; border-radius: 16px;
outline: 1px solid #e4e7ec; outline: 1px solid #e4e7ec;
max-width: 100%;
object-fit: contain;
} }
pre { pre {

View File

@@ -12,5 +12,6 @@ export * from './useCurrentSection'
export * from './usePreload' export * from './usePreload'
export * from './useCarousel' export * from './useCarousel'
export * from './usePrompt' export * from './usePrompt'
export * from './useIsDraggingOnElement'
export * from './useCountdown' export * from './useCountdown'

View File

@@ -0,0 +1,76 @@
import { MutableRefObject, useEffect, useRef, useState } from "react";
function addEventListener<K extends keyof HTMLElementEventMap>(element: HTMLElement, type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions) {
element.addEventListener(type, listener, options);
return () => element.removeEventListener(type, listener, options);
}
function setImmediate(callback: (...args: any[]) => void, ...args: any[]) {
let cancelled = false;
Promise.resolve().then(() => cancelled || callback(...args));
return () => {
cancelled = true;
};
}
function noop() { }
function handleDragOver(ev: DragEvent) {
ev.preventDefault();
ev.dataTransfer!.dropEffect = 'copy';
}
export const useIsDraggingOnElement = (options?: Partial<{
ref: MutableRefObject<HTMLElement>
}>) => {
const listenersRef = useRef<any[]>([]);
const [isDragging, setIsDragging] = useState(false);
useEffect(() => {
let count = 0;
let cancelImmediate = noop;
const element = options?.ref?.current ?? document as unknown as HTMLElement;
listenersRef.current = [
addEventListener(element, 'dragover', handleDragOver),
addEventListener(element, 'dragenter', ev => {
ev.preventDefault();
if (count === 0) {
setIsDragging(true)
}
++count;
}),
addEventListener(element, 'dragleave', ev => {
ev.preventDefault();
cancelImmediate = setImmediate(() => {
--count;
if (count === 0) {
setIsDragging(false)
}
})
}),
addEventListener(element, 'drop', ev => {
ev.preventDefault();
cancelImmediate();
if (count > 0) {
count = 0;
setIsDragging(false)
}
}),
]
return () => {
listenersRef.current.forEach(f => f());
}
}, [options?.ref])
return isDragging
}

View File

@@ -16,8 +16,10 @@ import "src/styles/index.scss";
import 'react-loading-skeleton/dist/skeleton.css' import 'react-loading-skeleton/dist/skeleton.css'
import { ApolloProvider } from '@apollo/client'; import { ApolloProvider } from '@apollo/client';
import { apolloClient } from '../apollo'; import { apolloClient } from '../apollo';
import { FormProvider, useForm, UseFormProps } from 'react-hook-form'; import { Controller, FormProvider, useForm, UseFormProps } from 'react-hook-form';
import ModalsContainer from 'src/Components/Modals/ModalsContainer/ModalsContainer'; import ModalsContainer from 'src/Components/Modals/ModalsContainer/ModalsContainer';
import { ToastContainer } from 'react-toastify';
import { NotificationsService } from 'src/services';
// Enable the Mocks Service Worker // Enable the Mocks Service Worker
@@ -63,6 +65,11 @@ export const WrapperDecorator: DecoratorFn = (Story, options) => {
effect='solid' effect='solid'
delayShow={1000} delayShow={1000}
/> />
<ToastContainer
{...NotificationsService.defaultOptions}
newestOnTop={false}
limit={2}
/>
</> </>
); );
} }
@@ -112,16 +119,42 @@ export const centerDecorator: DecoratorFn = (Story) => {
</div> </div>
} }
export function WrapForm<T = any>(options?: Partial<UseFormProps<T>>): DecoratorFn { export function WrapForm<T = any>(options?: Partial<UseFormProps<T> & { logValues: boolean }>): DecoratorFn {
const Func: DecoratorFn = (Story) => { const Func: DecoratorFn = (Story) => {
const methods = useForm<T>(options); const methods = useForm<T>(options);
if (options?.logValues) {
console.log(methods.watch())
}
return <FormProvider {...methods} > return <FormProvider {...methods} >
<Story /> <Story onChang />
</FormProvider> </FormProvider>
} }
return Func return Func
} }
export function WrapFormController<T = any>(options: Partial<UseFormProps<T> & { logValues: boolean }> & { name: string }): DecoratorFn {
const Func: DecoratorFn = (Story) => {
const methods = useForm<T>(options);
if (options?.logValues) {
console.log(methods.watch(options.name as any))
}
return <Controller
control={methods.control}
name={options.name as any}
render={({ field: { value, onChange, onBlur } }) =>
<Story controller={{ value, onChange, onBlur }} />
}
/>
}
return Func
}
export const WithModals: DecoratorFn = (Component) => <> export const WithModals: DecoratorFn = (Component) => <>
<Component /> <Component />

View File

@@ -0,0 +1 @@
export * from './misc';

View File

@@ -0,0 +1,12 @@
import * as yup from "yup";
export const imageSchema = yup.object().shape({
id: yup.string().nullable(true),
name: yup.string().nullable(true),
url: yup.string().trim().required().url(),
});
export const tagSchema = yup.object().shape({
title: yup.string().trim().min(2).required(),
});