Merge branch 'feedback' into 'dev'
trying to get into (#81) See merge request discoursio/discoursio-webapp!7
This commit is contained in:
commit
ab7f2b0260
6
.flake8
Normal file
6
.flake8
Normal file
|
@ -0,0 +1,6 @@
|
|||
[flake8]
|
||||
ignore = E203,W504,W191,W503
|
||||
exclude = .git,__pycache__,orm/rbac.py
|
||||
max-complexity = 12
|
||||
max-line-length = 108
|
||||
indent-string = ' '
|
|
@ -1,3 +1,10 @@
|
|||
[0.7.1]
|
||||
[+] reactions CRUL
|
||||
[+] api/upload with storj
|
||||
[+] api/feedback
|
||||
[+] bumped astro pkgs versions
|
||||
[+] graphql ws subs
|
||||
|
||||
[0.7.0]
|
||||
[+] inbox: context provider, chats
|
||||
[+] comments: show
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import MG from 'mailgun.js'
|
||||
import fd from 'form-data'
|
||||
const MG = require('mailgun.js')
|
||||
const fd = require('form-data')
|
||||
|
||||
const mgOptions = {
|
||||
key: process.env.MAILGUN_API_KEY,
|
||||
|
@ -9,7 +9,7 @@ const mgOptions = {
|
|||
|
||||
const messageData = (subject, text) => {
|
||||
return {
|
||||
from: 'Feedback Robot <robot@discours.io>',
|
||||
from: 'Discours Feedback Robot <robot@discours.io>',
|
||||
to: 'welcome@discours.io',
|
||||
subject,
|
||||
text
|
||||
|
@ -19,12 +19,7 @@ export default async function handler(req, res) {
|
|||
const { contact, subject, message } = req.query
|
||||
try {
|
||||
const mailgun = new MG(fd)
|
||||
const client = mailgun.client({
|
||||
username: mgOptions.username,
|
||||
key: mgOptions.key
|
||||
//url?: string;
|
||||
//public_key?: string;
|
||||
})
|
||||
const client = mailgun.client(mgOptions)
|
||||
const data = messageData(`${contact}: ${subject}`, message)
|
||||
client.messages.create(mgOptions.domain, data).then(console.log).catch(console.error)
|
||||
} catch (error) {
|
||||
|
|
32
api/newsletter.js
Normal file
32
api/newsletter.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
const MG = require('mailgun.js')
|
||||
const fd = require('form-data')
|
||||
|
||||
const mgOptions = {
|
||||
key: process.env.MAILGUN_API_KEY,
|
||||
domain: process.env.MAILGUN_DOMAIN,
|
||||
username: 'discoursio' // FIXME
|
||||
}
|
||||
|
||||
export default async (req, res) => {
|
||||
const { email } = req.query
|
||||
const mailgun = new MG(fd)
|
||||
const client = mailgun.client(mgOptions)
|
||||
|
||||
try {
|
||||
const response = await client.lists.members.createMember(mgOptions.domain, {
|
||||
address: email,
|
||||
subscribed: true,
|
||||
upsert: 'yes'
|
||||
})
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'Email added to newsletter list'
|
||||
})
|
||||
} catch (error) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
/* eslint-disable unicorn/no-empty-file */
|
||||
/*
|
||||
import MG from 'mailgun.js'
|
||||
|
||||
const mailgun = new MG({})
|
||||
const options = {
|
||||
apiKey: process.env.MAILGUN_API_KEY,
|
||||
domain: process.env.MAILGUN_DOMAIN
|
||||
}
|
||||
const list = mailgun.lists(process.env.MAILGUN_LIST_ID) // 'services@discours.io'
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { email } = req.body.data
|
||||
list.members().create({ subscribed: true, address: email }, (err, data) => {
|
||||
if (err) {
|
||||
console.error('[newsletter ] error', err)
|
||||
res.status(500)
|
||||
} else {
|
||||
res.status(200).json(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
export const handler = (req, res) => {
|
||||
return 'WIP'
|
||||
}
|
83
api/upload.py
Normal file
83
api/upload.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
from flask import Flask, request, jsonify
|
||||
from werkzeug.utils import secure_filename
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError, WaiterError
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
session = boto3.Session()
|
||||
storj_resource = session.resource('s3')
|
||||
storj_client = boto3.client('s3',
|
||||
aws_access_key_id=os.environ['STORJ_ACCESS_KEY'],
|
||||
aws_secret_access_key=os.environ['STORJ_SECRET_KEY'],
|
||||
endpoint_url=os.environ['STORJ_END_POINT']
|
||||
)
|
||||
|
||||
|
||||
def upload_storj(filecontent, filename, bucket_name):
|
||||
head = None
|
||||
bucket_obj = None
|
||||
try:
|
||||
bucket = storj_resource.Bucket(bucket_name)
|
||||
except ClientError:
|
||||
bucket = None
|
||||
|
||||
try:
|
||||
# In case filename already exists, get current etag to check if the
|
||||
# contents change after upload
|
||||
head = storj_client.head_object(Bucket=bucket_name, Key=filename)
|
||||
except ClientError:
|
||||
etag = ''
|
||||
else:
|
||||
etag = head['ETag'].strip('"')
|
||||
|
||||
try:
|
||||
bucket_obj = bucket.Object(filename)
|
||||
except (ClientError, AttributeError):
|
||||
bucket_obj = None
|
||||
|
||||
try:
|
||||
# Use the upload_fileobj method to safely upload the file
|
||||
storj_client.upload_fileobj(
|
||||
Fileobj=filecontent,
|
||||
Bucket=bucket_name,
|
||||
Key=filename
|
||||
)
|
||||
except (ClientError, AttributeError):
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
bucket_obj.wait_until_exists(IfNoneMatch=etag)
|
||||
except WaiterError:
|
||||
pass
|
||||
else:
|
||||
head = storj_client.head_object(Bucket=bucket_name, Key=filename)
|
||||
return head
|
||||
|
||||
|
||||
@app.route('/api/upload', methods=['post'])
|
||||
def upload():
|
||||
print(request.files.to_dict())
|
||||
# check if the post request has the file part
|
||||
if 'file' not in request.files:
|
||||
return {'error': 'No file part'}, 400
|
||||
file = request.files.get('file')
|
||||
if file:
|
||||
# save the file
|
||||
filename = secure_filename(file.name or file.filename)
|
||||
# if user does not select file, browser also
|
||||
# submit a empty part without filename
|
||||
if not filename:
|
||||
return {'error': 'No selected file'}, 400
|
||||
else:
|
||||
# Save the file to a temporary location
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = os.path.join(temp_dir, filename)
|
||||
file.save(temp_path)
|
||||
# Open the file in binary mode
|
||||
with open(temp_path, 'rb') as filecontent:
|
||||
result = upload_storj(filecontent, filename, 'discoursio')
|
||||
else:
|
||||
return {'error': 'No selected file'}, 400
|
||||
return {'message': 'File uploaded', 'result': jsonify(result)}, 200
|
|
@ -1,85 +0,0 @@
|
|||
import { Writable } from 'stream'
|
||||
import formidable from 'formidable'
|
||||
import { S3Client } from '@aws-sdk/client-s3'
|
||||
import { Upload } from '@aws-sdk/lib-storage'
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false
|
||||
}
|
||||
}
|
||||
|
||||
const BUCKET_NAME = process.env.S3_BUCKET || 'discours-io'
|
||||
const s3 = new S3Client({
|
||||
region: process.env.S3_REGION || 'eu-west-1',
|
||||
credentials: {
|
||||
accessKeyId: process.env.S3_ACCESS_KEY,
|
||||
secretAccessKey: process.env.S3_SECRET_KEY
|
||||
}
|
||||
})
|
||||
|
||||
const formidableConfig = {
|
||||
keepExtensions: true,
|
||||
maxFileSize: 10_000_000,
|
||||
maxFieldsSize: 10_000_000,
|
||||
maxFields: 7,
|
||||
allowEmptyFiles: false,
|
||||
multiples: false
|
||||
}
|
||||
|
||||
const formidablePromise = async (req, opts) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const form = formidable(opts)
|
||||
|
||||
form.parse(req, (err, fields, files) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
return resolve({ fields, files })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const fileConsumer = (acc) => {
|
||||
return new Writable({
|
||||
write: (chunk, _enc, next) => {
|
||||
acc.push(chunk)
|
||||
next()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handler(req, res) {
|
||||
if (req.method === 'POST') {
|
||||
try {
|
||||
const chunks = []
|
||||
const { fields, files }: any = await formidablePromise(req, {
|
||||
...formidableConfig,
|
||||
// consume this, otherwise formidable tries to save the file to disk
|
||||
fileWriteStreamHandler: () => fileConsumer(chunks)
|
||||
})
|
||||
|
||||
const data = Buffer.concat(chunks)
|
||||
|
||||
const params = {
|
||||
Bucket: process.env.S3_BUCKET || 'discours-io',
|
||||
Key: fields.name + '.' + fields.ext,
|
||||
Body: data,
|
||||
ACL: 'public-read',
|
||||
'Content-Type': fields.type
|
||||
}
|
||||
|
||||
const upload = new Upload({ params, client: s3 })
|
||||
await upload.done()
|
||||
// console.log(upload)
|
||||
const { singleUploadResult: result }: any = upload
|
||||
return res.status(200).json(result.Location)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(405).end()
|
||||
}
|
||||
|
||||
export default handler
|
115
package.json
115
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "discoursio-webapp",
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.1",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
@ -25,6 +25,7 @@
|
|||
"preview": "astro preview",
|
||||
"server": "node server/server.mjs",
|
||||
"start": "astro dev",
|
||||
"start:dev": "cross-env PUBLIC_API_URL=http://127.0.0.1:8080 vercel dev",
|
||||
"start:local": "cross-env PUBLIC_API_URL=http://127.0.0.1:8080 astro dev",
|
||||
"start:production": "cross-env PUBLIC_API_URL=https://v2.discours.io astro dev",
|
||||
"start:staging": "cross-env PUBLIC_API_URL=https://testapi.discours.io astro dev",
|
||||
|
@ -35,65 +36,66 @@
|
|||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.216.0",
|
||||
"@aws-sdk/lib-storage": "^3.223.0",
|
||||
"@connorskees/grass": "^0.12.0",
|
||||
"@solid-primitives/share": "^2.0.1",
|
||||
"astro-seo-meta": "^2.0.0",
|
||||
"formidable": "^2.1.1",
|
||||
"mailgun.js": "^8.0.2"
|
||||
"mailgun.js": "^8.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/solid-js": "^1.2.3",
|
||||
"@astrojs/vercel": "^2.3.3",
|
||||
"@babel/core": "^7.20.2",
|
||||
"@graphql-codegen/cli": "^2.13.12",
|
||||
"@graphql-codegen/typescript": "^2.8.2",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.7",
|
||||
"@astrojs/solid-js": "^2.0.0",
|
||||
"@astrojs/vercel": "^3.0.0",
|
||||
"@babel/core": "^7.20.12",
|
||||
"@graphql-codegen/cli": "^2.16.4",
|
||||
"@graphql-codegen/typescript": "^2.8.7",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.12",
|
||||
"@graphql-codegen/typescript-urql": "^3.7.3",
|
||||
"@graphql-codegen/urql-introspection": "^2.2.1",
|
||||
"@graphql-tools/url-loader": "^7.16.16",
|
||||
"@graphql-tools/url-loader": "^7.17.3",
|
||||
"@graphql-typed-document-node/core": "^3.1.1",
|
||||
"@nanostores/router": "^0.7.0",
|
||||
"@nanostores/solid": "^0.3.0",
|
||||
"@nanostores/router": "^0.8.0",
|
||||
"@nanostores/solid": "^0.3.2",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@solid-devtools/debugger": "^0.15.1",
|
||||
"@solid-devtools/logger": "^0.5.0",
|
||||
"@solid-primitives/memo": "^1.1.2",
|
||||
"@solid-primitives/storage": "^1.3.3",
|
||||
"@solid-primitives/memo": "^1.1.3",
|
||||
"@solid-primitives/share": "^2.0.1",
|
||||
"@solid-primitives/storage": "^1.3.4",
|
||||
"@solid-primitives/upload": "^0.0.105",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/express": "^4.17.15",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.43.0",
|
||||
"@urql/core": "^3.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
||||
"@typescript-eslint/parser": "^5.48.2",
|
||||
"@urql/core": "^3.1.1",
|
||||
"@urql/devtools": "^2.0.3",
|
||||
"@urql/exchange-graphcache": "^5.0.5",
|
||||
"astro": "^1.6.8",
|
||||
"astro-eslint-parser": "^0.9.0",
|
||||
"@urql/exchange-graphcache": "^5.0.8",
|
||||
"astro": "^2.0.2",
|
||||
"astro-eslint-parser": "^0.11.0",
|
||||
"astro-seo-meta": "^2.0.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bootstrap": "^5.2.2",
|
||||
"bootstrap": "^5.2.3",
|
||||
"clsx": "^1.2.1",
|
||||
"cookie": "^0.5.0",
|
||||
"cookie-signature": "^1.2.0",
|
||||
"cosmiconfig-toml-loader": "^1.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint-config-stylelint": "^17.0.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.2",
|
||||
"eslint-plugin-astro": "^0.21.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-stylelint": "^17.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint-plugin-astro": "^0.23.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-solid": "^0.8.0",
|
||||
"eslint-plugin-sonarjs": "^0.16.0",
|
||||
"eslint-plugin-unicorn": "^45.0.0",
|
||||
"eslint-plugin-solid": "^0.9.4",
|
||||
"eslint-plugin-sonarjs": "^0.18.0",
|
||||
"eslint-plugin-unicorn": "^45.0.2",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-sse": "^1.3.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.11.2",
|
||||
"hast-util-select": "^5.0.2",
|
||||
"husky": "^8.0.2",
|
||||
"hast-util-select": "^5.0.4",
|
||||
"husky": "^8.0.3",
|
||||
"idb": "^7.1.1",
|
||||
"jest": "^29.3.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"lint-staged": "^13.1.0",
|
||||
"loglevel": "^1.8.1",
|
||||
"loglevel-plugin-prefix": "^0.8.4",
|
||||
"markdown-it": "^13.0.1",
|
||||
|
@ -103,11 +105,11 @@
|
|||
"markdown-it-replace-link": "^1.1.0",
|
||||
"nanostores": "^0.7.1",
|
||||
"orderedmap": "^2.1.0",
|
||||
"postcss": "^8.4.19",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-modules": "5.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier": "^2.8.3",
|
||||
"prettier-eslint": "^15.0.1",
|
||||
"prosemirror-commands": "^1.3.1",
|
||||
"prosemirror-commands": "^1.5.0",
|
||||
"prosemirror-dropcursor": "^1.6.1",
|
||||
"prosemirror-example-setup": "^1.2.1",
|
||||
"prosemirror-gapcursor": "^1.3.1",
|
||||
|
@ -116,37 +118,32 @@
|
|||
"prosemirror-keymap": "^1.2.0",
|
||||
"prosemirror-markdown": "^1.10.1",
|
||||
"prosemirror-menu": "^1.2.1",
|
||||
"prosemirror-model": "^1.18.2",
|
||||
"prosemirror-model": "^1.19.0",
|
||||
"prosemirror-schema-list": "^1.2.2",
|
||||
"prosemirror-state": "^1.4.2",
|
||||
"prosemirror-view": "^1.29.1",
|
||||
"prosemirror-view": "^1.30.0",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-visualizer": "^5.8.3",
|
||||
"rollup-plugin-visualizer": "^5.9.0",
|
||||
"sass": "1.32.13",
|
||||
"solid-devtools": "^0.22.0",
|
||||
"solid-js": "^1.6.2",
|
||||
"solid-js-form": "^0.1.5",
|
||||
"solid-jsx": "^0.9.1",
|
||||
"solid-social": "^0.9.0",
|
||||
"solid-utils": "^0.8.1",
|
||||
"sort-package-json": "^2.1.0",
|
||||
"stylelint": "^14.15.0",
|
||||
"solid-js": "^1.6.9",
|
||||
"sort-package-json": "^2.3.0",
|
||||
"stylelint": "^14.16.1",
|
||||
"stylelint-config-css-modules": "^4.1.0",
|
||||
"stylelint-config-prettier-scss": "^0.0.1",
|
||||
"stylelint-config-standard-scss": "^6.1.0",
|
||||
"stylelint-order": "^5.0.0",
|
||||
"stylelint-order": "^6.0.1",
|
||||
"stylelint-scss": "^4.3.0",
|
||||
"swiper": "^8.4.4",
|
||||
"swiper": "^8.4.7",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.8.4",
|
||||
"undici": "^5.12.0",
|
||||
"typescript": "^4.9.4",
|
||||
"undici": "^5.15.1",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"uuid": "^9.0.0",
|
||||
"vite": "^3.2.4",
|
||||
"ws": "^8.11.0",
|
||||
"vite": "^3.2.5",
|
||||
"ws": "^8.12.0",
|
||||
"y-prosemirror": "^1.2.0",
|
||||
"y-protocols": "^1.0.5",
|
||||
"y-webrtc": "^10.2.3",
|
||||
"yjs": "^13.5.42"
|
||||
"y-webrtc": "^10.2.4",
|
||||
"yjs": "^13.5.44"
|
||||
}
|
||||
}
|
||||
|
|
4981
pnpm-lock.yaml
4981
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Flask==2.2.2
|
||||
boto3
|
|
@ -24,7 +24,7 @@ type Props = {
|
|||
isArticleAuthor?: boolean
|
||||
}
|
||||
|
||||
const Comment = (props: Props) => {
|
||||
export const Comment = (props: Props) => {
|
||||
const [isReplyVisible, setIsReplyVisible] = createSignal(false)
|
||||
const [loading, setLoading] = createSignal<boolean>(false)
|
||||
const [submitted, setSubmitted] = createSignal<boolean>(false)
|
||||
|
|
|
@ -8,13 +8,14 @@ export default () => {
|
|||
const [title, setTitle] = createSignal('')
|
||||
const subscribe = async () => {
|
||||
setTitle(t('...subscribing'))
|
||||
const r = await fetch(`/maillist?email=${emailElement?.value}`)
|
||||
const r = await fetch(`/api/newsletter?email=${emailElement?.value}`)
|
||||
setTitle(r.ok ? t('You are subscribed') : '')
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={styles.subscribeForm}>
|
||||
<input type="email" name="email" ref={emailElement} placeholder={t('Fill email')} value={title()} />
|
||||
<label for="email">{title()}</label>
|
||||
<input type="email" name="email" ref={emailElement} placeholder={t('Fill email')} />
|
||||
<button
|
||||
class={clsx(styles.button, 'button--light')}
|
||||
onClick={() => emailElement?.value && subscribe()}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { createSignal, Show } from 'solid-js'
|
||||
import { PageWrap } from '../../_shared/PageWrap'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { showModal } from '../../../stores/ui'
|
||||
|
||||
// const title = t('Terms of use')
|
||||
|
||||
|
|
|
@ -7,14 +7,14 @@ import { For, createSignal, Show, onMount } from 'solid-js'
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './Settings.module.scss'
|
||||
import { useProfileForm } from '../../../context/profile'
|
||||
import { createFileUploader } from '@solid-primitives/upload'
|
||||
import validateUrl from '../../../utils/validateUrl'
|
||||
|
||||
export const ProfileSettingsPage = (props: PageProps) => {
|
||||
const [addLinkForm, setAddLinkForm] = createSignal<boolean>(false)
|
||||
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
|
||||
const { form, updateFormField, submit, slugError } = useProfileForm()
|
||||
const handleChangeSocial = (value) => {
|
||||
let updateForm: HTMLFormElement
|
||||
const handleChangeSocial = (value: string) => {
|
||||
if (validateUrl(value)) {
|
||||
updateFormField('links', value)
|
||||
setAddLinkForm(false)
|
||||
|
@ -26,28 +26,32 @@ export const ProfileSettingsPage = (props: PageProps) => {
|
|||
event.preventDefault()
|
||||
submit(form)
|
||||
}
|
||||
|
||||
const { files, selectFiles: selectFilesAsync } = createFileUploader({ accept: 'image/*' })
|
||||
|
||||
const handleUpload = () => {
|
||||
selectFilesAsync(async ([{ source, name, size, file }]) => {
|
||||
const image = { source, name, size, file }
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('type', file.type)
|
||||
formData.append('name', image.source.split('/').pop())
|
||||
formData.append('ext', image.name.split('.').pop())
|
||||
const resp = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
const url = await resp.json()
|
||||
updateFormField('userpic', url)
|
||||
} catch (error) {
|
||||
console.error('[upload] error', error)
|
||||
let userpicFile: HTMLInputElement
|
||||
const handleFileUpload = async (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
console.log(formData)
|
||||
const response = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
const json = await response.json()
|
||||
console.debug(json)
|
||||
}
|
||||
const handleUserpicUpload = async (ev) => {
|
||||
// TODO: show progress
|
||||
console.debug('handleUserpicUpload')
|
||||
try {
|
||||
const f = ev.target.files[0]
|
||||
if (f) await handleFileUpload(f)
|
||||
} catch (error) {
|
||||
console.error('[upload] error', error)
|
||||
}
|
||||
}
|
||||
|
||||
const [hostname, setHostname] = createSignal('new.discours.io')
|
||||
onMount(() => setHostname(window?.location.host))
|
||||
|
||||
|
@ -65,12 +69,24 @@ export const ProfileSettingsPage = (props: PageProps) => {
|
|||
<div class="col-md-10 col-lg-9 col-xl-8">
|
||||
<h1>{t('Profile settings')}</h1>
|
||||
<p class="description">{t('Here you can customize your profile the way you want.')}</p>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<form ref={updateForm} onSubmit={handleSubmit} enctype="multipart/form-data">
|
||||
<h4>{t('Userpic')}</h4>
|
||||
<div class="pretty-form__item">
|
||||
<div class={styles.avatarContainer}>
|
||||
<img class={styles.avatar} src={form.userpic} alt={form.name} />
|
||||
<input type="button" class={styles.avatarInput} onClick={handleUpload} />
|
||||
<img
|
||||
class={styles.avatar}
|
||||
src={form.userpic}
|
||||
alt={form.name}
|
||||
onClick={() => userpicFile.click()}
|
||||
/>
|
||||
<input
|
||||
ref={userpicFile}
|
||||
type="file"
|
||||
name="file"
|
||||
value="file"
|
||||
hidden
|
||||
onChange={handleUserpicUpload}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<h4>{t('Name')}</h4>
|
||||
|
|
|
@ -20,7 +20,7 @@ import { Popup } from '../_shared/Popup'
|
|||
import { AuthorCard } from '../Author/Card'
|
||||
import { loadReactionsBy, REACTIONS_AMOUNT_PER_PAGE } from '../../stores/zine/reactions'
|
||||
import { apiClient } from '../../utils/apiClient'
|
||||
import Comment from '../Article/Comment'
|
||||
import { Comment } from '../Article/Comment'
|
||||
|
||||
// TODO: load reactions on client
|
||||
type AuthorProps = {
|
||||
|
@ -127,7 +127,7 @@ export const AuthorView = (props: AuthorProps) => {
|
|||
Популярное
|
||||
</button>
|
||||
</li>
|
||||
*/}
|
||||
*/}
|
||||
<li classList={{ selected: searchParams().by === 'about' }}>
|
||||
<button type="button" onClick={() => changeSearchParam('by', 'about')}>
|
||||
О себе
|
||||
|
|
|
@ -7,7 +7,7 @@ import { ArticleCard } from '../Feed/Card'
|
|||
import { AuthorCard } from '../Author/Card'
|
||||
import { t } from '../../utils/intl'
|
||||
import { FeedSidebar } from '../Feed/Sidebar'
|
||||
import Comment from '../Article/Comment'
|
||||
import { Comment as CommentCard } from '../Article/Comment'
|
||||
import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
|
||||
import { useReactionsStore } from '../../stores/zine/reactions'
|
||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||
|
@ -131,29 +131,13 @@ export const FeedView = () => {
|
|||
</Show>
|
||||
</div>
|
||||
|
||||
<aside class={clsx('col-md-4', styles.feedAside)}>
|
||||
<aside class={clsx('col-md-3', styles.feedAside)}>
|
||||
<section class={styles.asideSection}>
|
||||
<h4>{t('Comments')}</h4>
|
||||
<ul class={stylesArticle.comments}>
|
||||
<For each={topComments().filter((item) => item.body)}>
|
||||
{(comment) => {
|
||||
return (
|
||||
<li class={styles.comment}>
|
||||
<div class={clsx('text-truncate', styles.commentBody)} innerHTML={comment.body} />
|
||||
<AuthorCard
|
||||
author={comment.createdBy as Author}
|
||||
isFeedMode={true}
|
||||
compact={true}
|
||||
hideFollow={true}
|
||||
/>
|
||||
<div class={clsx('text-truncate', styles.commentArticleTitle)}>
|
||||
<a href={`/${comment.shout.slug}`}>{comment.shout.title}</a>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</ul>
|
||||
<For each={topComments()}>
|
||||
{/*FIXME: different components/better comment props*/}
|
||||
{(comment) => <CommentCard comment={comment} reactions={[]} compact={true} />}
|
||||
</For>
|
||||
</section>
|
||||
|
||||
<Show when={topTopics().length > 0}>
|
||||
|
|
|
@ -143,9 +143,9 @@ export const InboxView = () => {
|
|||
return b.updatedAt - a.updatedAt
|
||||
})
|
||||
if (sortByPerToPer()) {
|
||||
return sorted.filter((chat) => chat.title.trim().length === 0)
|
||||
return sorted.filter((chat) => Boolean(chat.title?.trim()))
|
||||
} else if (sortByGroup()) {
|
||||
return sorted.filter((chat) => chat.title.trim().length > 0)
|
||||
return sorted.filter((chat) => chat.title?.trim().length > 0)
|
||||
} else {
|
||||
return sorted
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ export const InboxView = () => {
|
|||
|
||||
const handleKeyDown = async (event) => {
|
||||
if (event.keyCode === 13 && event.shiftKey) return
|
||||
if (event.keyCode === 13 && !event.shiftKey && postMessageText().trim().length > 0) {
|
||||
if (event.keyCode === 13 && !event.shiftKey && postMessageText()?.trim().length > 0) {
|
||||
event.preventDefault()
|
||||
handleSubmit()
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { createContext, createEffect, createSignal, useContext } from 'solid-js'
|
||||
import type { Accessor, JSX } from 'solid-js'
|
||||
// import { createChatClient } from '../graphql/privateGraphQLClient'
|
||||
import { Accessor, createMemo, JSX } from 'solid-js'
|
||||
import { createContext, createSignal, useContext } from 'solid-js'
|
||||
import { createChatClient } from '../graphql/privateGraphQLClient'
|
||||
import type { Chat, Message, MutationCreateMessageArgs } from '../graphql/types.gen'
|
||||
import { apiClient } from '../utils/apiClient'
|
||||
// import newMessage from '../graphql/subs/new-message'
|
||||
// import type { Client } from '@urql/core'
|
||||
import newMessage from '../graphql/subs/new-message'
|
||||
import type { Client } from '@urql/core'
|
||||
import { pipe, subscribe } from 'wonka'
|
||||
import { loadMessages } from '../stores/inbox'
|
||||
|
||||
|
@ -29,7 +29,7 @@ export function useInbox() {
|
|||
export const InboxProvider = (props: { children: JSX.Element }) => {
|
||||
const [chats, setChats] = createSignal<Chat[]>([])
|
||||
const [messages, setMessages] = createSignal<Message[]>([])
|
||||
// const subclient = createMemo<Client>(() => createChatClient())
|
||||
const subclient = createMemo<Client>(() => createChatClient())
|
||||
const loadChats = async () => {
|
||||
try {
|
||||
const newChats = await apiClient.getChats({ limit: 50, offset: 0 })
|
||||
|
@ -72,7 +72,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => {
|
|||
}
|
||||
|
||||
const { unsubscribe } = pipe(
|
||||
() => null, // subclient().subscription(newMessage, {}),
|
||||
() => subclient().subscription(newMessage, {}),
|
||||
subscribe((result) => {
|
||||
console.info('[subscription]')
|
||||
console.debug(result)
|
||||
|
|
|
@ -6,8 +6,8 @@ import {
|
|||
subscriptionExchange,
|
||||
createClient
|
||||
} from '@urql/core'
|
||||
import { createClient as createSubClient } from 'graphql-sse'
|
||||
// import { createClient as createSubClient } from 'graphql-ws'
|
||||
// import { createClient as createSubClient } from 'graphql-sse'
|
||||
import { createClient as createSubClient } from 'graphql-ws'
|
||||
import { devtoolsExchange } from '@urql/devtools'
|
||||
import { isDev, apiBaseUrl } from '../utils/config'
|
||||
// import { cache } from './cache'
|
||||
|
@ -57,7 +57,7 @@ export const privateGraphQLClient = createClient(options)
|
|||
|
||||
export const createChatClient = () => {
|
||||
const subClient = createSubClient({
|
||||
url: apiBaseUrl + '/messages' // .replace('http', 'ws')
|
||||
url: (apiBaseUrl + '/messages').replace('http', 'ws')
|
||||
})
|
||||
|
||||
const subExchange = subscriptionExchange({
|
||||
|
|
21
vercel.json
21
vercel.json
|
@ -1,12 +1,17 @@
|
|||
{
|
||||
"functions": {
|
||||
"api/upload.ts": {
|
||||
"memory": 3008,
|
||||
"maxDuration": 30
|
||||
"routes": [
|
||||
{
|
||||
"src": "/api/upload",
|
||||
"headers": { "Content-Type": "application/json" },
|
||||
"dest": "api/upload.py"
|
||||
},
|
||||
"api/feedback.js": {
|
||||
"memory": 3008,
|
||||
"maxDuration": 30
|
||||
{
|
||||
"src": "/api/newsletter",
|
||||
"dest": "api/newsletter.js"
|
||||
},
|
||||
{
|
||||
"src": "/api/feedback",
|
||||
"dest": "api/feedback.js"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user