🎉 creates first draft of Bluesky webpart
This commit is contained in:
parent
94216dc26c
commit
b7247efc35
|
@ -0,0 +1,319 @@
|
|||
require('@rushstack/eslint-config/patch/modern-module-resolution');
|
||||
module.exports = {
|
||||
extends: ['@microsoft/eslint-config-spfx/lib/profiles/react'],
|
||||
parserOptions: { tsconfigRootDir: __dirname },
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
'parserOptions': {
|
||||
'project': './tsconfig.json',
|
||||
'ecmaVersion': 2018,
|
||||
'sourceType': 'module'
|
||||
},
|
||||
rules: {
|
||||
// Prevent usage of the JavaScript null value, while allowing code to access existing APIs that may require null. https://www.npmjs.com/package/@rushstack/eslint-plugin
|
||||
'@rushstack/no-new-null': 1,
|
||||
// Require Jest module mocking APIs to be called before any other statements in their code block. https://www.npmjs.com/package/@rushstack/eslint-plugin
|
||||
'@rushstack/hoist-jest-mock': 1,
|
||||
// Require regular expressions to be constructed from string constants rather than dynamically building strings at runtime. https://www.npmjs.com/package/@rushstack/eslint-plugin-security
|
||||
'@rushstack/security/no-unsafe-regexp': 1,
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
'@typescript-eslint/adjacent-overload-signatures': 1,
|
||||
// RATIONALE: Code is more readable when the type of every variable is immediately obvious.
|
||||
// Even if the compiler may be able to infer a type, this inference will be unavailable
|
||||
// to a person who is reviewing a GitHub diff. This rule makes writing code harder,
|
||||
// but writing code is a much less important activity than reading it.
|
||||
//
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
1,
|
||||
{
|
||||
'allowExpressions': true,
|
||||
'allowTypedFunctionExpressions': true,
|
||||
'allowHigherOrderFunctions': false
|
||||
}
|
||||
],
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
// Rationale to disable: although this is a recommended rule, it is up to dev to select coding style.
|
||||
// Set to 1 (warning) or 2 (error) to enable.
|
||||
'@typescript-eslint/explicit-member-accessibility': 0,
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
'@typescript-eslint/no-array-constructor': 1,
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
//
|
||||
// RATIONALE: The "any" keyword disables static type checking, the main benefit of using TypeScript.
|
||||
// This rule should be suppressed only in very special cases such as JSON.stringify()
|
||||
// where the type really can be anything. Even if the type is flexible, another type
|
||||
// may be more appropriate such as "unknown", "{}", or "Record<k,V>".
|
||||
'@typescript-eslint/no-explicit-any': 1,
|
||||
// RATIONALE: The #1 rule of promises is that every promise chain must be terminated by a catch()
|
||||
// handler. Thus wherever a Promise arises, the code must either append a catch handler,
|
||||
// or else return the object to a caller (who assumes this responsibility). Unterminated
|
||||
// promise chains are a serious issue. Besides causing errors to be silently ignored,
|
||||
// they can also cause a NodeJS process to terminate unexpectedly.
|
||||
'@typescript-eslint/no-floating-promises': 2,
|
||||
// RATIONALE: Catches a common coding mistake.
|
||||
'@typescript-eslint/no-for-in-array': 2,
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
'@typescript-eslint/no-misused-new': 2,
|
||||
// RATIONALE: The "namespace" keyword is not recommended for organizing code because JavaScript lacks
|
||||
// a "using" statement to traverse namespaces. Nested namespaces prevent certain bundler
|
||||
// optimizations. If you are declaring loose functions/variables, it's better to make them
|
||||
// static members of a class, since classes support property getters and their private
|
||||
// members are accessible by unit tests. Also, the exercise of choosing a meaningful
|
||||
// class name tends to produce more discoverable APIs: for example, search+replacing
|
||||
// the function "reverse()" is likely to return many false matches, whereas if we always
|
||||
// write "Text.reverse()" is more unique. For large scale organization, it's recommended
|
||||
// to decompose your code into separate NPM packages, which ensures that component
|
||||
// dependencies are tracked more conscientiously.
|
||||
//
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
'@typescript-eslint/no-namespace': [
|
||||
1,
|
||||
{
|
||||
'allowDeclarations': false,
|
||||
'allowDefinitionFiles': false
|
||||
}
|
||||
],
|
||||
// RATIONALE: Parameter properties provide a shorthand such as "constructor(public title: string)"
|
||||
// that avoids the effort of declaring "title" as a field. This TypeScript feature makes
|
||||
// code easier to write, but arguably sacrifices readability: In the notes for
|
||||
// "@typescript-eslint/member-ordering" we pointed out that fields are central to
|
||||
// a class's design, so we wouldn't want to bury them in a constructor signature
|
||||
// just to save some typing.
|
||||
//
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
// Set to 1 (warning) or 2 (error) to enable the rule
|
||||
'@typescript-eslint/parameter-properties': 0,
|
||||
// RATIONALE: When left in shipping code, unused variables often indicate a mistake. Dead code
|
||||
// may impact performance.
|
||||
//
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
1,
|
||||
{
|
||||
'vars': 'all',
|
||||
// Unused function arguments often indicate a mistake in JavaScript code. However in TypeScript code,
|
||||
// the compiler catches most of those mistakes, and unused arguments are fairly common for type signatures
|
||||
// that are overriding a base class method or implementing an interface.
|
||||
'args': 'none'
|
||||
}
|
||||
],
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
'@typescript-eslint/no-use-before-define': [
|
||||
2,
|
||||
{
|
||||
'functions': false,
|
||||
'classes': true,
|
||||
'variables': true,
|
||||
'enums': true,
|
||||
'typedefs': true
|
||||
}
|
||||
],
|
||||
// Disallows require statements except in import statements.
|
||||
// In other words, the use of forms such as var foo = require("foo") are banned. Instead use ES6 style imports or import foo = require("foo") imports.
|
||||
'@typescript-eslint/no-var-requires': 'error',
|
||||
// RATIONALE: The "module" keyword is deprecated except when describing legacy libraries.
|
||||
//
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
'@typescript-eslint/prefer-namespace-keyword': 1,
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
// Rationale to disable: it's up to developer to decide if he wants to add type annotations
|
||||
// Set to 1 (warning) or 2 (error) to enable the rule
|
||||
'@typescript-eslint/no-inferrable-types': 0,
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
// Rationale to disable: declaration of empty interfaces may be helpful for generic types scenarios
|
||||
'@typescript-eslint/no-empty-interface': 0,
|
||||
// RATIONALE: This rule warns if setters are defined without getters, which is probably a mistake.
|
||||
'accessor-pairs': 1,
|
||||
// RATIONALE: In TypeScript, if you write x["y"] instead of x.y, it disables type checking.
|
||||
'dot-notation': [
|
||||
1,
|
||||
{
|
||||
'allowPattern': '^_'
|
||||
}
|
||||
],
|
||||
// RATIONALE: Catches code that is likely to be incorrect
|
||||
'eqeqeq': 1,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'for-direction': 1,
|
||||
// RATIONALE: Catches a common coding mistake.
|
||||
'guard-for-in': 2,
|
||||
// RATIONALE: If you have more than 2,000 lines in a single source file, it's probably time
|
||||
// to split up your code.
|
||||
'max-lines': ['warn', { max: 2000 }],
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-async-promise-executor': 2,
|
||||
// RATIONALE: Deprecated language feature.
|
||||
'no-caller': 2,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-compare-neg-zero': 2,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-cond-assign': 2,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-constant-condition': 1,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-control-regex': 2,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-debugger': 1,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-delete-var': 2,
|
||||
// RATIONALE: Catches code that is likely to be incorrect
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-duplicate-case': 2,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-empty': 1,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-empty-character-class': 2,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-empty-pattern': 1,
|
||||
// RATIONALE: Eval is a security concern and a performance concern.
|
||||
'no-eval': 1,
|
||||
// RATIONALE: Catches code that is likely to be incorrect
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-ex-assign': 2,
|
||||
// RATIONALE: System types are global and should not be tampered with in a scalable code base.
|
||||
// If two different libraries (or two versions of the same library) both try to modify
|
||||
// a type, only one of them can win. Polyfills are acceptable because they implement
|
||||
// a standardized interoperable contract, but polyfills are generally coded in plain
|
||||
// JavaScript.
|
||||
'no-extend-native': 1,
|
||||
// Disallow unnecessary labels
|
||||
'no-extra-label': 1,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-fallthrough': 2,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-func-assign': 1,
|
||||
// RATIONALE: Catches a common coding mistake.
|
||||
'no-implied-eval': 2,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-invalid-regexp': 2,
|
||||
// RATIONALE: Catches a common coding mistake.
|
||||
'no-label-var': 2,
|
||||
// RATIONALE: Eliminates redundant code.
|
||||
'no-lone-blocks': 1,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-misleading-character-class': 2,
|
||||
// RATIONALE: Catches a common coding mistake.
|
||||
'no-multi-str': 2,
|
||||
// RATIONALE: It's generally a bad practice to call "new Thing()" without assigning the result to
|
||||
// a variable. Either it's part of an awkward expression like "(new Thing()).doSomething()",
|
||||
// or else implies that the constructor is doing nontrivial computations, which is often
|
||||
// a poor class design.
|
||||
'no-new': 1,
|
||||
// RATIONALE: Obsolete language feature that is deprecated.
|
||||
'no-new-func': 2,
|
||||
// RATIONALE: Obsolete language feature that is deprecated.
|
||||
'no-new-object': 2,
|
||||
// RATIONALE: Obsolete notation.
|
||||
'no-new-wrappers': 1,
|
||||
// RATIONALE: Catches code that is likely to be incorrect
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-octal': 2,
|
||||
// RATIONALE: Catches code that is likely to be incorrect
|
||||
'no-octal-escape': 2,
|
||||
// RATIONALE: Catches code that is likely to be incorrect
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-regex-spaces': 2,
|
||||
// RATIONALE: Catches a common coding mistake.
|
||||
'no-return-assign': 2,
|
||||
// RATIONALE: Security risk.
|
||||
'no-script-url': 1,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-self-assign': 2,
|
||||
// RATIONALE: Catches a common coding mistake.
|
||||
'no-self-compare': 2,
|
||||
// RATIONALE: This avoids statements such as "while (a = next(), a && a.length);" that use
|
||||
// commas to create compound expressions. In general code is more readable if each
|
||||
// step is split onto a separate line. This also makes it easier to set breakpoints
|
||||
// in the debugger.
|
||||
'no-sequences': 1,
|
||||
// RATIONALE: Catches code that is likely to be incorrect
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-shadow-restricted-names': 2,
|
||||
// RATIONALE: Obsolete language feature that is deprecated.
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-sparse-arrays': 2,
|
||||
// RATIONALE: Although in theory JavaScript allows any possible data type to be thrown as an exception,
|
||||
// such flexibility adds pointless complexity, by requiring every catch block to test
|
||||
// the type of the object that it receives. Whereas if catch blocks can always assume
|
||||
// that their object implements the "Error" contract, then the code is simpler, and
|
||||
// we generally get useful additional information like a call stack.
|
||||
'no-throw-literal': 2,
|
||||
// RATIONALE: Catches a common coding mistake.
|
||||
'no-unmodified-loop-condition': 1,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-unsafe-finally': 2,
|
||||
// RATIONALE: Catches a common coding mistake.
|
||||
'no-unused-expressions': 1,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-unused-labels': 1,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-useless-catch': 1,
|
||||
// RATIONALE: Avoids a potential performance problem.
|
||||
'no-useless-concat': 1,
|
||||
// RATIONALE: The "var" keyword is deprecated because of its confusing "hoisting" behavior.
|
||||
// Always use "let" or "const" instead.
|
||||
//
|
||||
// STANDARDIZED BY: @typescript-eslint\eslint-plugin\dist\configs\recommended.json
|
||||
'no-var': 2,
|
||||
// RATIONALE: Generally not needed in modern code.
|
||||
'no-void': 1,
|
||||
// RATIONALE: Obsolete language feature that is deprecated.
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'no-with': 2,
|
||||
// RATIONALE: Makes logic easier to understand, since constants always have a known value
|
||||
// @typescript-eslint\eslint-plugin\dist\configs\eslint-recommended.js
|
||||
'prefer-const': 1,
|
||||
// RATIONALE: Catches a common coding mistake where "resolve" and "reject" are confused.
|
||||
'promise/param-names': 2,
|
||||
// RATIONALE: Catches code that is likely to be incorrect
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'require-atomic-updates': 2,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'require-yield': 1,
|
||||
// "Use strict" is redundant when using the TypeScript compiler.
|
||||
'strict': [
|
||||
2,
|
||||
'never'
|
||||
],
|
||||
// RATIONALE: Catches code that is likely to be incorrect
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
'use-isnan': 2,
|
||||
// STANDARDIZED BY: eslint\conf\eslint-recommended.js
|
||||
// Set to 1 (warning) or 2 (error) to enable.
|
||||
// Rationale to disable: !!{}
|
||||
'no-extra-boolean-cast': 0,
|
||||
// ====================================================================
|
||||
// @microsoft/eslint-plugin-spfx
|
||||
// ====================================================================
|
||||
'@microsoft/spfx/import-requires-chunk-name': 1,
|
||||
'@microsoft/spfx/no-require-ensure': 2,
|
||||
'@microsoft/spfx/pair-react-dom-render-unmount': 1
|
||||
}
|
||||
},
|
||||
{
|
||||
// For unit tests, we can be a little bit less strict. The settings below revise the
|
||||
// defaults specified in the extended configurations, as well as above.
|
||||
files: [
|
||||
// Test files
|
||||
'*.test.ts',
|
||||
'*.test.tsx',
|
||||
'*.spec.ts',
|
||||
'*.spec.tsx',
|
||||
|
||||
// Facebook convention
|
||||
'**/__mocks__/*.ts',
|
||||
'**/__mocks__/*.tsx',
|
||||
'**/__tests__/*.ts',
|
||||
'**/__tests__/*.tsx',
|
||||
|
||||
// Microsoft convention
|
||||
'**/test/*.ts',
|
||||
'**/test/*.tsx'
|
||||
],
|
||||
rules: {}
|
||||
}
|
||||
]
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
release
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
.heft
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
||||
|
||||
# Environment variables
|
||||
.env
|
|
@ -0,0 +1,16 @@
|
|||
!dist
|
||||
config
|
||||
|
||||
gulpfile.js
|
||||
|
||||
release
|
||||
src
|
||||
temp
|
||||
|
||||
tsconfig.json
|
||||
tslint.json
|
||||
|
||||
*.log
|
||||
|
||||
.yo-rc.json
|
||||
.vscode
|
|
@ -0,0 +1 @@
|
|||
v18.20.4
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"whichFolder": "subdir",
|
||||
"solutionName": "BlueskyViewer",
|
||||
"componentType": "webpart",
|
||||
"template": "react",
|
||||
"componentName": "BlueSky",
|
||||
"plusBeta": false,
|
||||
"isCreatingSolution": true,
|
||||
"nodeVersion": "18.20.4",
|
||||
"sdksVersions": {
|
||||
"@microsoft/microsoft-graph-client": "3.0.2",
|
||||
"@microsoft/teams-js": "2.24.0"
|
||||
},
|
||||
"version": "1.20.0",
|
||||
"libraryName": "bluesky-viewer",
|
||||
"libraryId": "e6645216-311a-4108-b981-19acbaf75ca8",
|
||||
"environment": "spo",
|
||||
"packageManager": "npm",
|
||||
"solutionShortDescription": "BlueskyViewer description",
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
# bluesky-viewer
|
||||
|
||||
## Summary
|
||||
|
||||
Short summary on functionality and used technologies.
|
||||
|
||||
[picture of the solution in action, if possible]
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
|
||||
![version](https://img.shields.io/badge/version-1.20.0-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
- [SharePoint Framework](https://aka.ms/spfx)
|
||||
- [Microsoft 365 tenant](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
|
||||
|
||||
> Get your own free development tenant by subscribing to [Microsoft 365 developer program](http://aka.ms/o365devprogram)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
> Any special pre-requisites?
|
||||
|
||||
## Solution
|
||||
|
||||
| Solution | Author(s) |
|
||||
| ----------- | ------------------------------------------------------- |
|
||||
| folder name | Author details (name, company, twitter alias with link) |
|
||||
|
||||
## Version history
|
||||
|
||||
| Version | Date | Comments |
|
||||
| ------- | ---------------- | --------------- |
|
||||
| 1.1 | March 10, 2021 | Update comment |
|
||||
| 1.0 | January 29, 2021 | Initial release |
|
||||
|
||||
## Disclaimer
|
||||
|
||||
**THIS CODE IS PROVIDED _AS IS_ WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
---
|
||||
|
||||
## Minimal Path to Awesome
|
||||
|
||||
- Clone this repository
|
||||
- Ensure that you are at the solution folder
|
||||
- in the command-line run:
|
||||
- **npm install**
|
||||
- **gulp serve**
|
||||
|
||||
> Include any additional steps as needed.
|
||||
|
||||
## Features
|
||||
|
||||
Description of the extension that expands upon high-level summary above.
|
||||
|
||||
This extension illustrates the following concepts:
|
||||
|
||||
- topic 1
|
||||
- topic 2
|
||||
- topic 3
|
||||
|
||||
> Notice that better pictures and documentation will increase the sample usage and the value you are providing for others. Thanks for your submissions advance.
|
||||
|
||||
> Share your web part with others through Microsoft 365 Patterns and Practices program to get visibility and exposure. More details on the community, open-source projects and other activities from http://aka.ms/m365pnp.
|
||||
|
||||
## References
|
||||
|
||||
- [Getting started with SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/set-up-your-developer-tenant)
|
||||
- [Building for Microsoft teams](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/build-for-teams-overview)
|
||||
- [Use Microsoft Graph in your solution](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/get-started/using-microsoft-graph-apis)
|
||||
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/publish-to-marketplace-overview)
|
||||
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"blue-sky-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/blueSky/BlueSkyWebPart.js",
|
||||
"manifest": "./src/webparts/blueSky/BlueSkyWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"BlueSkyWebPartStrings": "lib/webparts/blueSky/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./release/assets/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "bluesky-viewer",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"REACT_APP_BSKY_HANDLE": "luisedev.bsky.social",
|
||||
"REACT_APP_BSKY_APP_PASSWORD": "w5sn-i7oa-3rys-jcjm"
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "bluesky-viewer-client-side-solution",
|
||||
"id": "e6645216-311a-4108-b981-19acbaf75ca8",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"skipFeatureDeployment": true,
|
||||
"isDomainIsolated": false,
|
||||
"developer": {
|
||||
"name": "",
|
||||
"websiteUrl": "",
|
||||
"privacyUrl": "",
|
||||
"termsOfUseUrl": "",
|
||||
"mpnId": "Undefined-1.20.0"
|
||||
},
|
||||
"metadata": {
|
||||
"shortDescription": {
|
||||
"default": "BlueskyViewer description"
|
||||
},
|
||||
"longDescription": {
|
||||
"default": "BlueskyViewer description"
|
||||
},
|
||||
"screenshotPaths": [],
|
||||
"videoUrl": "",
|
||||
"categories": []
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"title": "bluesky-viewer Feature",
|
||||
"description": "The feature that activates elements of the bluesky-viewer solution.",
|
||||
"id": "d7842dc3-5ef1-4cf8-a806-99143273c9ba",
|
||||
"version": "1.0.0.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/bluesky-viewer.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/sass.schema.json"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://hscluise.sharepoint.com/_layouts/workbench.aspx"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"REACT_APP_BSKY_HANDLE": "luisedev.bsky.social",
|
||||
"REACT_APP_BSKY_APP_PASSWORD": "w5sn-i7oa-3rys-jcjm"
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
|
||||
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
|
||||
var getTasks = build.rig.getTasks;
|
||||
build.rig.getTasks = function () {
|
||||
var result = getTasks.call(build.rig);
|
||||
|
||||
result.set('serve', result.get('serve-deprecated'));
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
build.initialize(require('gulp'));
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "bluesky-viewer",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18.17.1 <19.0.0"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.13.15",
|
||||
"@fluentui/react": "^8.106.4",
|
||||
"@fluentui/react-cards": "^0.205.183",
|
||||
"@microsoft/sp-component-base": "1.20.0",
|
||||
"@microsoft/sp-core-library": "1.20.0",
|
||||
"@microsoft/sp-lodash-subset": "1.20.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.20.0",
|
||||
"@microsoft/sp-property-pane": "1.20.0",
|
||||
"@microsoft/sp-webpart-base": "1.20.0",
|
||||
"@pnp/graph": "^4.6.0",
|
||||
"@pnp/sp": "^4.6.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"tslib": "2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/eslint-config-spfx": "1.20.2",
|
||||
"@microsoft/eslint-plugin-spfx": "1.20.2",
|
||||
"@microsoft/rush-stack-compiler-4.7": "0.1.0",
|
||||
"@microsoft/sp-build-web": "1.20.2",
|
||||
"@microsoft/sp-module-interfaces": "1.20.2",
|
||||
"@rushstack/eslint-config": "4.0.1",
|
||||
"@types/react": "17.0.45",
|
||||
"@types/react-dom": "17.0.17",
|
||||
"@types/webpack-env": "~1.15.2",
|
||||
"ajv": "^6.12.5",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-react-hooks": "4.3.0",
|
||||
"gulp": "4.0.2",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "9806faed-7d68-4387-a905-8eb9991346ee",
|
||||
"alias": "BlueSkyWebPart",
|
||||
"componentType": "WebPart",
|
||||
// The "*" signifies that the version should be taken from the package.json
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
// If true, the component can only be installed on sites where Custom Script is allowed.
|
||||
// Components that allow authors to embed arbitrary script code should set this to true.
|
||||
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||
"requiresCustomScript": false,
|
||||
"supportedHosts": [
|
||||
"SharePointWebPart",
|
||||
"TeamsPersonalApp",
|
||||
"TeamsTab",
|
||||
"SharePointFullPage"
|
||||
],
|
||||
"supportsThemeVariants": true,
|
||||
"preconfiguredEntries": [
|
||||
{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Advanced
|
||||
"group": {
|
||||
"default": "Advanced"
|
||||
},
|
||||
"title": {
|
||||
"default": "BlueSky"
|
||||
},
|
||||
"description": {
|
||||
"default": "BlueSky description"
|
||||
},
|
||||
"officeFabricIconFontName": "Page",
|
||||
"properties": {
|
||||
"description": "Bluesky"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
type IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField
|
||||
} from '@microsoft/sp-property-pane';
|
||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||
|
||||
import * as strings from 'BlueSkyWebPartStrings';
|
||||
import BlueSky from './components/BlueSky';
|
||||
import { IBlueSkyProps } from './components/IBlueSkyProps';
|
||||
|
||||
export interface IBlueSkyWebPartProps {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export default class BlueSkyWebPart extends BaseClientSideWebPart<IBlueSkyWebPartProps> {
|
||||
|
||||
private _isDarkTheme: boolean = false;
|
||||
private _environmentMessage: string = '';
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IBlueSkyProps> = React.createElement(
|
||||
BlueSky,
|
||||
{
|
||||
description: this.properties.description,
|
||||
isDarkTheme: this._isDarkTheme,
|
||||
environmentMessage: this._environmentMessage,
|
||||
hasTeamsContext: !!this.context.sdks.microsoftTeams,
|
||||
userDisplayName: this.context.pageContext.user.displayName
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
return this._getEnvironmentMessage().then(message => {
|
||||
this._environmentMessage = message;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
private _getEnvironmentMessage(): Promise<string> {
|
||||
if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
|
||||
return this.context.sdks.microsoftTeams.teamsJs.app.getContext()
|
||||
.then(context => {
|
||||
let environmentMessage: string = '';
|
||||
switch (context.app.host.name) {
|
||||
case 'Office': // running in Office
|
||||
environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOffice : strings.AppOfficeEnvironment;
|
||||
break;
|
||||
case 'Outlook': // running in Outlook
|
||||
environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentOutlook : strings.AppOutlookEnvironment;
|
||||
break;
|
||||
case 'Teams': // running in Teams
|
||||
case 'TeamsModern':
|
||||
environmentMessage = this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentTeams : strings.AppTeamsTabEnvironment;
|
||||
break;
|
||||
default:
|
||||
environmentMessage = strings.UnknownEnvironment;
|
||||
}
|
||||
|
||||
return environmentMessage;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(this.context.isServedFromLocalhost ? strings.AppLocalEnvironmentSharePoint : strings.AppSharePointEnvironment);
|
||||
}
|
||||
|
||||
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
|
||||
if (!currentTheme) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._isDarkTheme = !!currentTheme.isInverted;
|
||||
const {
|
||||
semanticColors
|
||||
} = currentTheme;
|
||||
|
||||
if (semanticColors) {
|
||||
this.domElement.style.setProperty('--bodyText', semanticColors.bodyText || null);
|
||||
this.domElement.style.setProperty('--link', semanticColors.link || null);
|
||||
this.domElement.style.setProperty('--linkHovered', semanticColors.linkHovered || null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
header: {
|
||||
description: strings.PropertyPaneDescription
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.BasicGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('description', {
|
||||
label: strings.DescriptionFieldLabel
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,158 @@
|
|||
/* BlueSky.module.scss */
|
||||
|
||||
.card {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
margin: 10px;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
text-overflow: ellipsis;
|
||||
transition: transform 0.2s ease;
|
||||
white-space: pre-wrap;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.cardAuthor {
|
||||
color: #0078d4;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
margin-left: 10px; /* Add margin to separate the avatar and the name */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.cardAuthorContainer {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.cardAvatar {
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.cardContent {
|
||||
color: #000; /* Ensure text is always black */
|
||||
font-size: 1.1rem;
|
||||
font-weight: 400; /* Regular font weight for content */
|
||||
overflow: hidden;
|
||||
text-decoration: none !important; /* No underline */
|
||||
text-overflow: ellipsis;
|
||||
white-space: pre-wrap; /* Allows text to wrap */
|
||||
|
||||
a {
|
||||
color: #000 !important; /* Ensure links inside content are black */
|
||||
text-decoration: none !important; /* No underline */
|
||||
|
||||
&:hover,
|
||||
&:visited,
|
||||
&:link {
|
||||
color: #000 !important; /* Ensure color remains black on hover, visited, and link */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cardImage {
|
||||
border-radius: 8px;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.cardImageWrapper {
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: 8px;
|
||||
margin: 0 auto;
|
||||
padding-top: 56.25%; /* 16:9 Aspect Ratio */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cardImagesContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.cardLink {
|
||||
text-decoration: none !important; /* No underline */
|
||||
}
|
||||
|
||||
.cardTimestamp {
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.hashtag {
|
||||
color: #0078d4 !important; /* Ensure the color is blue */
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
margin-right: 5px;
|
||||
text-decoration: none !important; /* No underline */
|
||||
|
||||
&:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
&:visited {
|
||||
color: #0078d4 !important; /* Ensure the color remains the same for visited links */
|
||||
}
|
||||
}
|
||||
|
||||
.linkDescription {
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.linkImage {
|
||||
border-radius: 8px;
|
||||
height: auto;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.linkPreview {
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.linkTitle {
|
||||
color: #333;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.postStats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.statItem {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import React from 'react';
|
||||
import { Card, CardSection } from '@fluentui/react-cards';
|
||||
import { initializeIcons } from '@fluentui/react';
|
||||
import BlueSkyImageSection from './BlueSkyImageSection';
|
||||
import BlueSkyAuthorSection from './BlueSkyAuthorSection';
|
||||
import BlueSkyContentSection from './BlueSkyContentSection';
|
||||
import BlueSkyTimestampSection from './BlueSkyTimestampSection';
|
||||
import useAccessToken from './useAccessToken';
|
||||
import useBlueSkyPosts from './useBlueSkyPosts';
|
||||
import styles from './BlueSky.module.scss';
|
||||
import { IBlueSkyProps } from './IBlueSkyProps';
|
||||
|
||||
// Initialize Fluent UI icons
|
||||
initializeIcons();
|
||||
|
||||
const BlueSky: React.FC<IBlueSkyProps> = (props) => {
|
||||
const { accessToken, error: tokenError } = useAccessToken('your handle', 'your app password');
|
||||
const { posts, loading, error: postsError } = useBlueSkyPosts(accessToken);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{props.description}</h1>
|
||||
{loading && <p>Loading posts...</p>}
|
||||
{(tokenError || postsError) && <p style={{ color: 'red' }}>Error: {tokenError || postsError}</p>}
|
||||
|
||||
<div className={styles.container}>
|
||||
{posts.map((post) => {
|
||||
const lastUriSegment = post.uri.split('/').pop();
|
||||
const postUrl = `https://bsky.app/profile/${post.author.handle}/post/${lastUriSegment}`;
|
||||
return (
|
||||
<a key={post.id} href={postUrl} target="_blank" rel="noopener noreferrer" className={styles.cardLink}>
|
||||
<Card className={styles.card}>
|
||||
<CardSection>
|
||||
<BlueSkyAuthorSection avatar={post.avatar} author={post.author.displayName} />
|
||||
</CardSection>
|
||||
<CardSection>
|
||||
<BlueSkyContentSection content={post.content} />
|
||||
</CardSection>
|
||||
<CardSection>
|
||||
<BlueSkyImageSection images={post.embed?.images} external={post.embed?.external} did={post.did} />
|
||||
</CardSection>
|
||||
<CardSection>
|
||||
<BlueSkyTimestampSection timestamp={post.timestamp} />
|
||||
</CardSection>
|
||||
</Card>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlueSky;
|
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import { Text } from '@fluentui/react';
|
||||
import styles from './BlueSky.module.scss';
|
||||
|
||||
interface BlueSkyAuthorSectionProps {
|
||||
avatar: string;
|
||||
author: string;
|
||||
}
|
||||
|
||||
const BlueSkyAuthorSection: React.FC<BlueSkyAuthorSectionProps> = ({ avatar, author }) => {
|
||||
console.log('Author:', author); // Log the author prop to the console
|
||||
|
||||
return (
|
||||
<div className={styles.cardAuthorContainer}>
|
||||
<img src={avatar} alt={`${author}'s avatar`} className={styles.cardAvatar} />
|
||||
<Text className={styles.cardAuthor}>{author}</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlueSkyAuthorSection;
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import styles from './BlueSky.module.scss';
|
||||
|
||||
interface BlueSkyContentSectionProps {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const BlueSkyContentSection: React.FC<BlueSkyContentSectionProps> = ({ content }) => {
|
||||
const renderContentWithHashtags = (text: string): React.ReactNode[] => {
|
||||
const hashtagPattern = /#\w+/g;
|
||||
const parts = text.split(hashtagPattern);
|
||||
const hashtags = text.match(hashtagPattern) || [];
|
||||
|
||||
return parts.reduce((acc, part, index) => {
|
||||
acc.push(<span key={`part-${index}`}>{part}</span>);
|
||||
if (hashtags[index]) {
|
||||
acc.push(
|
||||
<span key={`hashtag-${index}`} className={styles.hashtag}>
|
||||
{hashtags[index]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, [] as React.ReactNode[]);
|
||||
};
|
||||
|
||||
return <div className={styles.cardContent}>{renderContentWithHashtags(content)}</div>;
|
||||
};
|
||||
|
||||
export default BlueSkyContentSection;
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import styles from './BlueSky.module.scss';
|
||||
|
||||
interface BlueSkyImage {
|
||||
alt: string;
|
||||
aspectRatio: {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
image: {
|
||||
$type: string;
|
||||
mimeType: string;
|
||||
ref: {
|
||||
$link: string;
|
||||
};
|
||||
size: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface BlueSkyExternal {
|
||||
description: string;
|
||||
thumb: {
|
||||
$type: string;
|
||||
mimeType: string;
|
||||
ref: {
|
||||
$link: string;
|
||||
};
|
||||
size: number;
|
||||
};
|
||||
title: string;
|
||||
uri: string;
|
||||
}
|
||||
|
||||
interface BlueSkyImageSectionProps {
|
||||
images?: BlueSkyImage[];
|
||||
external?: BlueSkyExternal;
|
||||
did: string;
|
||||
}
|
||||
|
||||
const BlueSkyImageSection: React.FC<BlueSkyImageSectionProps> = ({ images, external, did }) => {
|
||||
return (
|
||||
<div className={styles.cardImagesContainer}>
|
||||
{images?.map((image, index) => {
|
||||
const imageUrl = `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${image.image.ref.$link}@jpeg`;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={styles.cardImageWrapper}
|
||||
style={{ backgroundImage: `url(${imageUrl})` }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{external && (
|
||||
<div
|
||||
className={styles.cardImageWrapper}
|
||||
style={{ backgroundImage: `url(https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${external.thumb.ref.$link}@jpeg)` }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlueSkyImageSection;
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import { Text } from '@fluentui/react';
|
||||
import styles from './BlueSky.module.scss';
|
||||
|
||||
interface BlueSkyTimestampSectionProps {
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
const BlueSkyTimestampSection: React.FC<BlueSkyTimestampSectionProps> = ({ timestamp }) => {
|
||||
return (
|
||||
<div>
|
||||
<Text className={styles.cardTimestamp}>{new Date(timestamp).toLocaleString()}</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlueSkyTimestampSection;
|
|
@ -0,0 +1,7 @@
|
|||
export interface IBlueSkyProps {
|
||||
description: string;
|
||||
isDarkTheme: boolean;
|
||||
environmentMessage: string;
|
||||
hasTeamsContext: boolean;
|
||||
userDisplayName: string;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
const getAccessToken = async (handle: string, appPassword: string): Promise<string> => {
|
||||
const response = await fetch('https://bsky.social/xrpc/com.atproto.server.createSession', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ identifier: handle, password: appPassword }),
|
||||
});
|
||||
if (!response.ok) throw new Error(`Failed to get access token: ${response.statusText}`);
|
||||
const data = await response.json();
|
||||
return data.accessJwt;
|
||||
};
|
||||
|
||||
const useAccessToken = (handle: string, appPassword: string): { accessToken: string | undefined, error: string | undefined } => {
|
||||
const [accessToken, setAccessToken] = useState<string | undefined>(undefined);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchToken = async (): Promise<void> => {
|
||||
try {
|
||||
const token = await getAccessToken(handle, appPassword);
|
||||
setAccessToken(token);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) setError("Failed to retrieve access token: " + err.message);
|
||||
}
|
||||
};
|
||||
fetchToken().catch((err) => console.error("Failed to fetch token:", err));
|
||||
}, [handle, appPassword]);
|
||||
|
||||
return { accessToken, error };
|
||||
};
|
||||
|
||||
export default useAccessToken;
|
|
@ -0,0 +1,216 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface BlueSkyPost {
|
||||
id: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
author: {
|
||||
displayName: string;
|
||||
avatar?: string;
|
||||
did: string;
|
||||
handle: string;
|
||||
};
|
||||
avatar: string;
|
||||
embed?: {
|
||||
images?: {
|
||||
alt: string;
|
||||
aspectRatio: {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
image: {
|
||||
$type: string;
|
||||
mimeType: string;
|
||||
ref: {
|
||||
$link: string;
|
||||
};
|
||||
size: number;
|
||||
};
|
||||
}[];
|
||||
external?: {
|
||||
description: string;
|
||||
thumb: {
|
||||
$type: string;
|
||||
mimeType: string;
|
||||
ref: {
|
||||
$link: string;
|
||||
};
|
||||
size: number;
|
||||
};
|
||||
title: string;
|
||||
uri: string;
|
||||
};
|
||||
};
|
||||
did: string;
|
||||
uri: string;
|
||||
replyCount: number;
|
||||
reshareCount: number;
|
||||
likeCount: number;
|
||||
}
|
||||
|
||||
interface BlueSkyApiResponse {
|
||||
feed: { post: BlueSkyPostItem }[];
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
interface BlueSkyPostItem {
|
||||
cid: string;
|
||||
uri: string;
|
||||
record: {
|
||||
text: string;
|
||||
createdAt: string;
|
||||
embed?: {
|
||||
images?: {
|
||||
alt: string;
|
||||
aspectRatio: {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
image: {
|
||||
$type: string;
|
||||
mimeType: string;
|
||||
ref: {
|
||||
$link: string;
|
||||
};
|
||||
size: number;
|
||||
};
|
||||
}[];
|
||||
external?: {
|
||||
description: string;
|
||||
thumb: {
|
||||
$type: string;
|
||||
mimeType: string;
|
||||
ref: {
|
||||
$link: string;
|
||||
};
|
||||
size: number;
|
||||
};
|
||||
title: string;
|
||||
uri: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
author: {
|
||||
displayName: string;
|
||||
avatar?: string;
|
||||
did: string;
|
||||
handle: string;
|
||||
};
|
||||
replyCount: number;
|
||||
reshareCount: number;
|
||||
likeCount: number;
|
||||
}
|
||||
|
||||
const fetchWithRateLimitRetry = async (url: string, options: RequestInit, retries = 3, delay = 2000): Promise<unknown> => {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (response.status === 429) throw new Error("Rate Limit Exceeded");
|
||||
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
|
||||
const jsonData = await response.json();
|
||||
return jsonData;
|
||||
} catch (error: unknown) {
|
||||
if (retries > 0 && error instanceof Error && error.message === "Rate Limit Exceeded") {
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
return fetchWithRateLimitRetry(url, options, retries - 1, delay * 2);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const useBlueSkyPosts = (accessToken: string | undefined): { posts: BlueSkyPost[], loading: boolean, error: string | undefined } => {
|
||||
const [posts, setPosts] = useState<BlueSkyPost[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken) return;
|
||||
|
||||
const fetchBlueSkyPosts = async (): Promise<void> => {
|
||||
setLoading(true);
|
||||
setError(undefined);
|
||||
|
||||
const cacheKey = 'blueSkyPosts';
|
||||
const cacheExpiryKey = 'blueSkyPostsExpiry';
|
||||
const cacheExpiryTime = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
const cachedPosts = localStorage.getItem(cacheKey);
|
||||
const cachedExpiry = localStorage.getItem(cacheExpiryKey);
|
||||
|
||||
if (cachedPosts && cachedExpiry && Date.now() < parseInt(cachedExpiry, 10)) {
|
||||
console.log("Using cached posts:", JSON.parse(cachedPosts)); // Log the cached posts
|
||||
setPosts(JSON.parse(cachedPosts));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const hashtags = ["#Microsoft365", "#SPFx", "#SharePoint"];
|
||||
const filteredPosts: BlueSkyPost[] = [];
|
||||
let cursor: string | undefined = undefined;
|
||||
|
||||
try {
|
||||
while (filteredPosts.length < 10) { // Limit to 10 posts
|
||||
const url: string = `https://bsky.social/xrpc/app.bsky.feed.getTimeline${cursor ? `?cursor=${cursor}` : ''}`;
|
||||
const data = await fetchWithRateLimitRetry(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}) as BlueSkyApiResponse;
|
||||
|
||||
console.log("Full API Response:", data); // Log the full response
|
||||
|
||||
const feed = data.feed;
|
||||
|
||||
// Filter posts that contain specified hashtags
|
||||
const matchingPosts = feed
|
||||
.filter(item => hashtags.some(tag => item.post.record.text.includes(tag)))
|
||||
.map(item => ({
|
||||
id: item.post.cid,
|
||||
content: item.post.record.text,
|
||||
timestamp: item.post.record.createdAt,
|
||||
author: {
|
||||
displayName: item.post.author.displayName,
|
||||
avatar: item.post.author.avatar || '',
|
||||
did: item.post.author.did,
|
||||
handle: item.post.author.handle,
|
||||
},
|
||||
avatar: item.post.author.avatar || '',
|
||||
embed: item.post.record.embed,
|
||||
did: item.post.author.did,
|
||||
uri: item.post.uri,
|
||||
replyCount: item.post.replyCount,
|
||||
reshareCount: item.post.reshareCount,
|
||||
likeCount: item.post.likeCount,
|
||||
}));
|
||||
|
||||
filteredPosts.push(...matchingPosts);
|
||||
|
||||
if (filteredPosts.length >= 10) break;
|
||||
cursor = data.cursor;
|
||||
if (!cursor) break;
|
||||
}
|
||||
|
||||
console.log("Filtered Posts:", filteredPosts); // Log the filtered posts
|
||||
|
||||
setPosts(filteredPosts.slice(0, 10));
|
||||
localStorage.setItem(cacheKey, JSON.stringify(filteredPosts.slice(0, 10)));
|
||||
localStorage.setItem(cacheExpiryKey, (Date.now() + cacheExpiryTime).toString());
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchBlueSkyPosts().catch((err) => console.error("Failed to fetch posts:", err));
|
||||
}, [accessToken]);
|
||||
|
||||
return { posts, loading, error };
|
||||
};
|
||||
|
||||
export default useBlueSkyPosts;
|
|
@ -0,0 +1,16 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"DescriptionFieldLabel": "Description Field",
|
||||
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
|
||||
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
|
||||
"AppLocalEnvironmentOffice": "The app is running on your local environment in office.com",
|
||||
"AppLocalEnvironmentOutlook": "The app is running on your local environment in Outlook",
|
||||
"AppSharePointEnvironment": "The app is running on SharePoint page",
|
||||
"AppTeamsTabEnvironment": "The app is running in Microsoft Teams",
|
||||
"AppOfficeEnvironment": "The app is running in office.com",
|
||||
"AppOutlookEnvironment": "The app is running in Outlook",
|
||||
"UnknownEnvironment": "The app is running in an unknown environment"
|
||||
}
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
declare interface IBlueSkyWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
DescriptionFieldLabel: string;
|
||||
AppLocalEnvironmentSharePoint: string;
|
||||
AppLocalEnvironmentTeams: string;
|
||||
AppLocalEnvironmentOffice: string;
|
||||
AppLocalEnvironmentOutlook: string;
|
||||
AppSharePointEnvironment: string;
|
||||
AppTeamsTabEnvironment: string;
|
||||
AppOfficeEnvironment: string;
|
||||
AppOutlookEnvironment: string;
|
||||
UnknownEnvironment: string;
|
||||
}
|
||||
|
||||
declare module 'BlueSkyWebPartStrings' {
|
||||
const strings: IBlueSkyWebPartStrings;
|
||||
export = strings;
|
||||
}
|
||||
|
||||
declare module '@atproto/api';
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 249 B |
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-4.7/includes/tsconfig-web.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "lib",
|
||||
"inlineSources": false,
|
||||
"noImplicitAny": true,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@microsoft"
|
||||
],
|
||||
"types": [
|
||||
"webpack-env"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection",
|
||||
"es2015.promise",
|
||||
"es2015",
|
||||
"es2016"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue