Showing preview only (515K chars total). Download the full file or copy to clipboard to get everything.
Repository: hzeyuan/x-cards
Branch: master
Commit: a6c94888a84f
Files: 125
Total size: 476.8 KB
Directory structure:
gitextract_nbnj719d/
├── .github/
│ └── workflows/
│ └── submit.yml
├── .gitignore
├── .prettierrc.mjs
├── LICENSE
├── README.md
├── README_ZH.md
├── components/
│ └── ui/
│ ├── EditableButton.tsx
│ ├── accordion.tsx
│ ├── alert-dialog.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── color-picker.tsx
│ ├── dropdown-menu.tsx
│ ├── input.tsx
│ ├── popover.tsx
│ ├── radio-group.tsx
│ ├── select-position.tsx
│ ├── select.tsx
│ ├── slider.tsx
│ ├── sonner.tsx
│ ├── switch.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ ├── toast.tsx
│ ├── toaster.tsx
│ └── use-toast.ts
├── components.json
├── lib/
│ └── utils.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── popup.tsx
├── postcss.config.js
├── src/
│ ├── app/
│ │ ├── (app)/
│ │ │ ├── components/
│ │ │ │ ├── FrequentlyAskedQuestions.tsx
│ │ │ │ ├── GoogleFontSelector.tsx
│ │ │ │ ├── ImageLayout.tsx
│ │ │ │ ├── LazyLoadAnimatedSection.tsx
│ │ │ │ ├── ResultIcon.tsx
│ │ │ │ ├── card-generator/
│ │ │ │ │ ├── color.tsx
│ │ │ │ │ ├── controller/
│ │ │ │ │ │ ├── background-controller.tsx
│ │ │ │ │ │ ├── card-controller.tsx
│ │ │ │ │ │ ├── font-controller.tsx
│ │ │ │ │ │ ├── iframe-controller.tsx
│ │ │ │ │ │ └── input-controller.tsx
│ │ │ │ │ ├── display.tsx
│ │ │ │ │ ├── export-tab.tsx
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── twitter-card.tsx
│ │ │ │ │ └── wechat-card.tsx
│ │ │ │ ├── dynamic-style-tippy.tsx
│ │ │ │ ├── hero.tsx
│ │ │ │ ├── save-as-template-button.tsx
│ │ │ │ ├── sections/
│ │ │ │ │ ├── FeaturesGridSection.tsx
│ │ │ │ │ ├── features2.tsx
│ │ │ │ │ ├── footer.tsx
│ │ │ │ │ └── video.tsx
│ │ │ │ ├── template-list.tsx
│ │ │ │ └── x-form.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── request.tsx
│ │ ├── (extension)/
│ │ │ ├── independent/
│ │ │ │ ├── components/
│ │ │ │ │ └── index.tsx
│ │ │ │ └── page.tsx
│ │ │ └── welcome/
│ │ │ └── page.tsx
│ │ ├── api/
│ │ │ ├── license/
│ │ │ │ └── route.ts
│ │ │ └── x/
│ │ │ └── route.ts
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ └── utils/
│ │ ├── IFrameMessageSystem.ts
│ │ ├── element.ts
│ │ ├── export.ts
│ │ ├── format.ts
│ │ ├── image.ts
│ │ └── index.ts
│ ├── background/
│ │ ├── index.ts
│ │ └── messages/
│ │ ├── code.ts
│ │ └── tweet.ts
│ ├── components/
│ │ ├── extension/
│ │ │ ├── card-button.tsx
│ │ │ ├── input-code.tsx
│ │ │ ├── label-with-icon.tsx
│ │ │ ├── layout-options.tsx
│ │ │ ├── preset-color-list.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── tweet-manager.tsx
│ │ │ ├── use-tweet-collection.ts
│ │ │ └── x-cards-toast/
│ │ │ ├── font-control.tsx
│ │ │ ├── image-preview.tsx
│ │ │ ├── index.module.css
│ │ │ ├── index.tsx
│ │ │ ├── padding-control.tsx
│ │ │ ├── scale-control.tsx
│ │ │ └── tweet-control.tsx
│ │ ├── sortableList.tsx
│ │ └── ui/
│ │ ├── BentoGrid.tsx
│ │ ├── DotPattern.tsx
│ │ ├── acetenity-tabs.tsx
│ │ ├── animated-list.tsx
│ │ ├── animatedBeam.tsx
│ │ ├── api-key-panel.tsx
│ │ ├── bold-copy.tsx
│ │ ├── burnIn.tsx
│ │ ├── card.tsx
│ │ ├── chart.tsx
│ │ ├── fade-text.tsx
│ │ ├── grid-pattern.tsx
│ │ ├── grid.tsx
│ │ ├── icon.tsx
│ │ ├── loading-spinner.tsx
│ │ ├── marquee.tsx
│ │ ├── scroll-based-velocity.tsx
│ │ ├── shimmer-button.tsx
│ │ ├── text-generate-effect.tsx
│ │ └── underline-hover-text.tsx
│ ├── config/
│ │ └── site.ts
│ ├── contents/
│ │ ├── plasmo-overlay.css
│ │ ├── plasmo-overlay.tsx
│ │ ├── x-home.tsx
│ │ └── x.css
│ ├── hooks/
│ │ ├── useCardStore.tsx
│ │ └── useTemplatesStore.tsx
│ ├── lib/
│ │ └── BlurGradientBg.module.js
│ └── sandbox.tsx
├── tailwind.config.ts
├── tsconfig.json
└── vercel.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/submit.yml
================================================
name: "Submit to Web Store"
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Cache pnpm modules
uses: actions/cache@v3
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-
- uses: pnpm/action-setup@v2.2.4
with:
version: latest
run_install: true
- name: Use Node.js 16.x
uses: actions/setup-node@v3.4.1
with:
node-version: 16.x
cache: "pnpm"
- name: Build the extension
run: pnpm build
- name: Package the extension into a zip artifact
run: pnpm package
- name: Browser Platform Publish
uses: PlasmoHQ/bpp@v3
with:
keys: ${{ secrets.SUBMIT_KEYS }}
artifact: build/chrome-mv3-prod.zip
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
out/
build/
dist/
# plasmo
.plasmo
# typescript
.tsbuildinfo
.next
================================================
FILE: .prettierrc.mjs
================================================
/**
* @type {import('prettier').Options}
*/
export default {
printWidth: 80,
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: false,
trailingComma: "none",
bracketSpacing: true,
bracketSameLine: true,
plugins: ["@ianvs/prettier-plugin-sort-imports"],
importOrder: [
"<BUILTIN_MODULES>", // Node.js built-in modules
"<THIRD_PARTY_MODULES>", // Imports not matched by other special words or groups.
"", // Empty line
"^@plasmo/(.*)$",
"",
"^@plasmohq/(.*)$",
"",
"^~(.*)$",
"",
"^[./]"
]
}
================================================
FILE: LICENSE
================================================
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
================================================
FILE: README.md
================================================
<a name="readme-top"></a>
<div align="center">
<img src="assets/icon.png" width="32" >
<h1>X Cards</h1>
[English](README.md) | [中文](README_ZH.md)
[](https://youtu.be/okCIZrFrTCE)
easy to use x-cards in x.com
[![][vercel-shield]][vercel-link]
[![][share-x-shield]][share-x-link]
[![][share-whatsapp-shield]][share-whatsapp-link]
[![][share-reddit-shield]][share-reddit-link]
[![][share-weibo-shield]][share-weibo-link]
[![][share-linkedin-shield]][share-linkedin-link]
[github-issues-link]: https://github.com/hzeyuan/x-cards/issues
[github-contributors-shield]: https://img.shields.io/github/contributors/hzeyuan/OpenGPTS?color=c4f042&labelColor=black&style=flat-square
[github-contributors-link]: https://github.com/hzeyuan/OpenGPTS/graphs/contributors
[vercel-link]: https://x-cards.net
[vercel-shield]: https://img.shields.io/website?down_message=offline&label=vercel&labelColor=black&logo=vercel&style=flat-square&up_message=online&url=https://x-cards.net
[share-linkedin-link]: https://linkedin.com/feed
[share-linkedin-shield]: https://img.shields.io/badge/-share%20on%20linkedin-black?labelColor=black&logo=linkedin&logoColor=white&style=flat-square
[share-reddit-link]: https://www.reddit.com/submit?title=x-cards&url=https://github.com/hzeyuan/x-cards
[share-reddit-shield]: https://img.shields.io/badge/-share%20on%20reddit-black?labelColor=black&logo=reddit&logoColor=white&style=flat-square
[share-telegram-link]: https://t.me/share/url?text=x-cards&url=https://github.com/hzeyuan/x-cards
[share-telegram-shield]: https://img.shields.io/badge/-share%20on%20telegram-black?labelColor=black&logo=telegram&logoColor=white&style=flat-square
[share-weibo-link]: http://service.weibo.com/share/share.php?sharesource=weibo&title=x-cards
[share-weibo-shield]: https://img.shields.io/badge/-share%20on%20weibo-black?labelColor=black&logo=sinaweibo&logoColor=white&style=flat-square
[share-whatsapp-link]: https://api.whatsapp.com/send?text=x-cards
[share-whatsapp-shield]: https://img.shields.io/badge/-share%20on%20whatsapp-black?labelColor=black&logo=whatsapp&logoColor=white&style=flat-square
[share-x-link]: https://x.com/intent/tweet?hashtags=chatbot%2CchatGPT%2CopenAI&url=https://github.com/hzeyuan/x-cards
[share-x-shield]: https://img.shields.io/badge/-share%20on%20x-black?labelColor=black&logo=x&logoColor=white&style=flat-square
</div>
⚡ **X Cards** Share Tweet anywhere ,any format,
## Project Background
X is a source of information for many platforms,So this project came into being
## Changelog
<details>
<summary><strong>v0.0.3</strong></summary>
* Performance Optimization: Utilized web workers for image generation, addressing issues with blank spaces on X.com after scrolling down.
* Enhanced Padding Settings: Added the ability to adjust padding within the card for better layout control.
* Image Quality Settings: Introduced options to set the quality of generated images for export.
* Markdown Export: Now supports exporting content in Markdown format for easy integration into documentation or blogs.
* Font Size Adjustment: Added support for modifying font sizes to improve readability and customization.
* Interaction Optimization: After installing the plugin or clicking the icon, redirect to the welcome page.

</details>
<details>
<summary><strong>v0.0.2</strong></summary>
- Added real-time preview feature, now a toast in the upper right corner allows you to observe the generated card.
- Introduced customization for card background color.
- Customizable card width.
- Improved: Now clicking defaults to copying the image, rather than downloading the image.
- Fixed the issue of unable to fetch cover image for videos.
- Added support for fetching continuous posts.
- Enabled dynamic addition, deletion, dragging, and management of posts.

</details>
<details>
<summary><strong>v0.0.1</strong></summary>
- Easy to access, just a simple click away.
- Obtain videos, images, text, likes。
- Export in multiple formats, including JSON, Markdown, PNG, JPEG, and SVG.
</details>
## features
- Added real-time preview feature, now a toast in the upper right corner allows you to observe the generated card.
- Introduced customization for card background color.
- Customizable card width.
- Improved: Now clicking defaults to copying the image, rather than downloading the image.
- Fixed the issue of unable to fetch cover image for videos.
- Added support for fetching continuous posts.
- Enabled dynamic addition, deletion, dragging, and management of posts.
<br/>
## How to Use
### [chrome web Store](https://chromewebstore.google.com/detail/x-card/mbinooofmcjhjklihfejnkkebffceeop)
## or
1. Download Extension

2. url input:chrome://extensions/ in your chrome browser, and open the developer mode
3. unzip and Drag the extension file to the page

4. open x.com and browse the post, you will find your card button in the bottom right corner
## Development Guide
1. The project uses the Plasmo framework for rapid Chrome extension development.
2. Uses Next.js for frontend development.
3. Tailwind CSS and Shadcn as CSS frameworks.
4. Langchain for developing agents.
5. Deployed on Vercel.
Local development:
```bash
pnpm install
# Run frontend
npm run dev:next
# Run plugin
npm run dev:plasmo
```
## Starchart
[](https://star-history.com/#hzeyuan/x-cards&Date)
================================================
FILE: README_ZH.md
================================================
<a name="readme-top"></a>
<div align="center">
<img src="assets/icon.png" width="32" >
<h1>X Cards</h1>
[English](README.md) | [中文](README_ZH.md)
[](https://www.youtube.com/watch?v=v8iQV8ZoVBk)
轻松的X平台上使用x-cards
[![][vercel-shield]][vercel-link]
[![][share-x-shield]][share-x-link]
[![][share-whatsapp-shield]][share-whatsapp-link]
[![][share-reddit-shield]][share-reddit-link]
[![][share-weibo-shield]][share-weibo-link]
[![][share-linkedin-shield]][share-linkedin-link]
[github-issues-link]: https://github.com/hzeyuan/x-cards/issues
[github-contributors-shield]: https://img.shields.io/github/contributors/hzeyuan/OpenGPTS?color=c4f042&labelColor=black&style=flat-square
[github-contributors-link]: https://github.com/hzeyuan/OpenGPTS/graphs/contributors
[vercel-link]: https://x-cards.net
[vercel-shield]: https://img.shields.io/website?down_message=offline&label=vercel&labelColor=black&logo=vercel&style=flat-square&up_message=online&url=https://x-cards.net
[share-linkedin-link]: https://linkedin.com/feed
[share-linkedin-shield]: https://img.shields.io/badge/-share%20on%20linkedin-black?labelColor=black&logo=linkedin&logoColor=white&style=flat-square
[share-reddit-link]: https://www.reddit.com/submit?title=x-cards&url=https://github.com/hzeyuan/x-cards
[share-reddit-shield]: https://img.shields.io/badge/-share%20on%20reddit-black?labelColor=black&logo=reddit&logoColor=white&style=flat-square
[share-telegram-link]: https://t.me/share/url?text=x-cards&url=https://github.com/hzeyuan/x-cards
[share-telegram-shield]: https://img.shields.io/badge/-share%20on%20telegram-black?labelColor=black&logo=telegram&logoColor=white&style=flat-square
[share-weibo-link]: http://service.weibo.com/share/share.php?sharesource=weibo&title=x-cards
[share-weibo-shield]: https://img.shields.io/badge/-share%20on%20weibo-black?labelColor=black&logo=sinaweibo&logoColor=white&style=flat-square
[share-whatsapp-link]: https://api.whatsapp.com/send?text=x-cards
[share-whatsapp-shield]: https://img.shields.io/badge/-share%20on%20whatsapp-black?labelColor=black&logo=whatsapp&logoColor=white&style=flat-square
[share-x-link]: https://x.com/intent/tweet?hashtags=chatbot%2CchatGPT%2CopenAI&url=https://github.com/hzeyuan/x-cards
[share-x-shield]: https://img.shields.io/badge/-share%20on%20x-black?labelColor=black&logo=x&logoColor=white&style=flat-square
</div>
⚡ **X Cards** Share X anywhere ,any format,
## 项目背景
X是许多平台的信息来源,因此产生了这个项目。
## Changelog
<details>
<summary><strong>v0.0.3</strong></summary>
- 优化性能,使用web worker生成图片, 处理下拉后x.com空白问题。
- 增加padding设置,调整卡片内边距。
- 增加生成图片质量设置。
- 增加md格式导出。
- 支持调整字体大小。
- 交互优化,安装插件后或者,点击icon,跳转到welcome页面。
<details>
<summary><strong>v0.0.2</strong></summary>
- 新增实时预览功能,现在右上角有个toast可以观察到生成的卡片。
- 引入了自定义卡片背景颜色。
- 可自定义卡片宽度。
- 改进:现在点击默认复制图片,而不是下载图片。
- 修复了视频获取封面图的问题。
- 增加了连续帖子获取的支持。
- 实现了帖子的动态添加、删除、拖拽和管理。
</details>
<details>
<summary><strong>v0.0.1</strong></summary>
- 点击即可轻松访问。
- 获取视频、图片、文本、点赞等。
- 导出多种格式,包括JSON、Markdown、PNG、JPEG和SVG。
</details>
## features
- 简单易用,只需点击一下即生成卡片。
- 轻松获取视频、图片、文字、点赞和浏览历史。
- 支持多种格式导出,包括JSON、Markdown、PNG、JPEG和SVG。
- 模板功能,保存您经常使用的卡片样式。
<br/>
## How to Use
1. 点击下载插件

2. 浏览器输入 chrome://extensions/ 并打开开发者模式
3. 解压并,拖动整个文件夹到页面,如图:

4. 打开x.com并浏览帖子,您将在右下角找到您的卡片按钮,参考上方视频。
## 开发指南
本项目使用 Plasmo 框架进行快速 Chrome 扩展开发。
使用 Next.js 进行前端开发。
采用 Tailwind CSS 和 Shadcn 作为 CSS 框架。
使用 Langchain 开发智能代理。
部署在 Vercel 平台上。
Local development:
```bash
pnpm install
# Run frontend
npm run dev:next
# Run plugin
npm run dev:plasmo
```
## Starchart
[](https://star-history.com/#hzeyuan/x-cards&Date)
================================================
FILE: components/ui/EditableButton.tsx
================================================
import React, { useState, useRef } from 'react';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
const EditableButton = ({ text }) => {
const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(text);
const inputRef = useRef(null);
const handleClick = () => {
if (!isEditing) {
setIsEditing(true);
setTimeout(() => inputRef.current?.focus(), 0);
} else {
setIsEditing(false);
// 在这里可以添加保存或提交的逻辑
console.log('Submitted:', value);
}
};
const handleChange = (e) => {
setValue(e.target.value);
};
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
handleClick();
}
};
return (
<div className="relative inline-block">
{isEditing ? (
<Input
ref={inputRef}
type="text"
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
className="pr-20"
/>
) : (
<Button size="sm" onClick={handleClick} variant="outline">
{value}
</Button>
)}
{isEditing && (
<Button
onClick={handleClick}
className="absolute right-1 top-1/2 transform -translate-y-1/2"
size="sm"
>
保存
</Button>
)}
</div>
);
};
export default EditableButton;
================================================
FILE: components/ui/accordion.tsx
================================================
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
// className={cn("border-b", className)}
className={cn(
"w-full bg-secondary items-center cursor-pointer text-[13px] mb-3 px-4 py-0 rounded-md",
className,
)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-2 font-medium transition-all [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
================================================
FILE: components/ui/alert-dialog.tsx
================================================
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
================================================
FILE: components/ui/badge.tsx
================================================
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }
================================================
FILE: components/ui/button.tsx
================================================
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
================================================
FILE: components/ui/color-picker.tsx
================================================
'use client';
import { forwardRef, useEffect, useMemo, useRef, useState } from 'react';
import { HexColorPicker } from 'react-colorful';
import { cn } from '@/lib/utils';
import type { ButtonProps } from '@/components/ui/button';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { PopoverContent, PopoverTrigger, Popover } from './popover';
export function useForwardedRef<T>(ref: React.ForwardedRef<T>) {
const innerRef = useRef<T>(null);
useEffect(() => {
if (!ref) return;
if (typeof ref === 'function') {
ref(innerRef.current);
} else {
ref.current = innerRef.current;
}
});
return innerRef;
}
interface ColorPickerProps {
value: string;
onChange: (value: string) => void;
onBlur?: () => void;
}
const ColorPicker = forwardRef<
HTMLInputElement,
Omit<ButtonProps, 'value' | 'onChange' | 'onBlur'> & ColorPickerProps
>(
(
{ disabled, value, onChange, onBlur, name, className, ...props },
forwardedRef
) => {
const ref = useForwardedRef(forwardedRef);
const [open, setOpen] = useState(false);
const parsedValue = useMemo(() => {
return value || '#FFFFFF';
}, [value]);
return (
<Popover onOpenChange={setOpen} open={open}>
<PopoverTrigger asChild disabled={disabled} onBlur={onBlur}>
<Button
{...props}
className={cn('block', className)}
name={name}
onClick={() => {
setOpen(true);
}}
size='icon'
style={{
backgroundColor: parsedValue,
}}
variant='outline'
>
<div />
</Button>
</PopoverTrigger>
<PopoverContent className='w-full flex flex-col gap-y-2'>
<HexColorPicker color={parsedValue} onChange={onChange} />
<Input
maxLength={7}
onChange={(e) => {
onChange(e?.currentTarget?.value);
}}
ref={ref}
value={parsedValue}
/>
</PopoverContent>
</Popover>
);
}
);
ColorPicker.displayName = 'ColorPicker';
export { ColorPicker };
================================================
FILE: components/ui/dropdown-menu.tsx
================================================
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}
================================================
FILE: components/ui/input.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }
================================================
FILE: components/ui/popover.tsx
================================================
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }
================================================
FILE: components/ui/radio-group.tsx
================================================
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }
================================================
FILE: components/ui/select-position.tsx
================================================
"use client"
import { cn } from "@lib/utils"
import { useState } from "react"
export const SelectBackgroundPosition = ({ onChange }) => {
const [position, setPosition] = useState('center center')
const positions = [
{ value: "left top", title: "Top left" },
{ value: "center top", title: "Top center" },
{ value: "right top", title: "Top right" },
{ value: "left center", title: "Left center" },
{ value: "center center", title: "Center" },
{ value: "right center", title: "Right center" },
{ value: "left bottom", title: "Bottom left" },
{ value: "center bottom", title: "Bottom center" },
{ value: "right bottom", title: "Bottom right" }
]
const handleClick = (newPosition) => {
setPosition(newPosition)
onChange(newPosition)
}
return (
<div className="relative grid w-12 h-12 grid-cols-3 p-1 bg-white border border-gray-200 rounded-lg dark:border-gray-700 place-content-around place-items-center aspect-square dark:bg-gray-900 shadow hover:scale-[1.4] duration-300 ease-[cubic-bezier(.75,-0.5,0,1.75)]">
{positions.map(({ value, title }) => (
<div
key={value}
onClick={() => handleClick(value)}
title={title}
className={cn(
"w-[8px] h-[8px] rounded-full cursor-pointer",
position === value
? "bg-gray-800 dark:bg-gray-200"
: "bg-gray-300 hover:bg-gray-500 dark:hover:bg-gray-400 dark:bg-gray-600/50"
)}
/>
))}
</div>
)
}
================================================
FILE: components/ui/select.tsx
================================================
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}
================================================
FILE: components/ui/slider.tsx
================================================
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
{/* bg-secondary */}
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-white">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }
================================================
FILE: components/ui/sonner.tsx
================================================
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }
================================================
FILE: components/ui/switch.tsx
================================================
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-4 w-7 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-2.5 w-2.5 rounded-full bg-white ring-0 transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0 duration-250"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }
================================================
FILE: components/ui/tabs.tsx
================================================
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }
================================================
FILE: components/ui/textarea.tsx
================================================
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }
================================================
FILE: components/ui/toast.tsx
================================================
"use client"
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}
================================================
FILE: components/ui/toaster.tsx
================================================
"use client"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}
================================================
FILE: components/ui/use-toast.ts
================================================
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }
================================================
FILE: components.json
================================================
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}
================================================
FILE: lib/utils.ts
================================================
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
================================================
FILE: next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
================================================
FILE: next.config.js
================================================
// const isProd = process.env.NODE_ENV === 'production'
const isProd = false;
const bundleAnalyzer = require('@next/bundle-analyzer')
const withBundleAnalyzer = bundleAnalyzer({
enabled: false,
openAnalyzer: true,
})
module.exports = withBundleAnalyzer({
swcMinify: true,
crossOrigin: 'anonymous',
reactStrictMode: false,
env: {
STATIC_URL: isProd ? STATIC_URL : "http://localhost:3000",
},
// typescript: {
// ignoreBuildErrors: true,
// },
// webpack: (config) => {
// config.externals = [...config.externals, { canvas: 'canvas' }];
// return config;
// },
// experimental: {
// serverActions: {
// allowedOrigins: []
// },
// },
// async redirects() {
// return [
// {
// source: "/home",
// destination: "/",
// permanent: false,
// }]
// }
typescript: {
ignoreBuildErrors: true,
},
output: 'export',
// 禁用图像优化,因为它需要 Next.js 服务器
images: {
unoptimized: true,
},
webpack: (config, { isServer }) => {
if (isServer) {
// 在服务器端构建时忽略 API 路由
config.externals = config.externals || [];
config.externals.push((context, request, callback) => {
if (request.startsWith('pages/api/') || request.startsWith('app/api/')) {
return callback(null, `commonjs ${request}`);
}
callback();
});
}
return config;
},
})
================================================
FILE: package.json
================================================
{
"name": "X Cards Native Tweet Card service for X",
"displayName": "Seamlessly integrate card services directly on your X",
"version": "0.0.3",
"description": "Create stunning tweet cards effortlessly with X Cards on Twitter. Share your thoughts, ideas, and creations with style and flair",
"keywords": [
"x",
"card",
"share",
"anywhere"
],
"author": "yixtotieq@gmail.com",
"scripts": {
"dev": "run-p dev:*",
"dev:plasmo": "plasmo dev",
"dev:next": "next dev --port 1947",
"build": "plasmo build",
"start:next": "next start",
"build:next": "next build",
"package": "plasmo package"
},
"dependencies": {
"@csstools/convert-colors": "^2.0.0",
"@langchain/core": "^0.2.16",
"@langchain/openai": "^0.2.2",
"@next/env": "^14.2.5",
"@plasmohq/messaging": "0.6.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@supabase/supabase-js": "^2.45.1",
"@tippyjs/react": "^4.2.6",
"@vercel/analytics": "^1.3.1",
"@visactor/react-vchart": "^1.11.9",
"add": "^2.0.6",
"class-variance-authority": "^0.7.0",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
"file-saver": "^2.0.5",
"framer-motion": "^11.3.7",
"html-to-image": "^1.11.11",
"idb-keyval": "3.0.0",
"jsonrepair": "^3.8.0",
"jszip": "^3.10.1",
"langchain": "^0.2.10",
"lodash-es": "^4.17.21",
"lucide-react": "^0.408.0",
"markmap-common": "^0.17.0",
"markmap-lib": "^0.17.0",
"markmap-view": "^0.17.0",
"mini-svg-data-uri": "^1.4.4",
"modern-screenshot": "^4.4.39",
"next": "14.1.0",
"next-themes": "^0.3.0",
"p-limit": "^6.1.0",
"p-retry": "^6.2.0",
"plasmo": "0.88.0",
"popover": "^2.4.1",
"randomcolor": "^0.6.2",
"react": "18.2.0",
"react-accessible-treeview": "^2.9.1",
"react-color": "^2.19.3",
"react-colorful": "^5.6.1",
"react-day-picker": "^9.0.0",
"react-dom": "18.2.0",
"react-hot-toast": "^2.4.1",
"react-icon-cloud": "^4.1.4",
"react-intersection-observer": "^9.13.0",
"react-player": "^2.16.0",
"react-scroll-parallax": "^3.4.5",
"react-use-measure": "^2.1.1",
"react-virtual": "^2.10.4",
"recharts": "^2.12.7",
"sonner": "^1.5.0",
"styled-components": "^6.1.12",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^10.0.0",
"webextension-polyfill": "^0.12.0",
"zod": "^3.23.8",
"zustand": "^4.5.4"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "4.1.1",
"@next/bundle-analyzer": "^14.2.5",
"@types/chrome": "0.0.258",
"@types/lodash-es": "^4.17.12",
"@types/node": "20.11.5",
"@types/react": "18.2.48",
"@types/react-dom": "18.2.18",
"@types/webextension-polyfill": "^0.10.7",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"postcss-import": "^15.1.0",
"postcss-nested": "^6.2.0",
"prettier": "3.2.4",
"tailwindcss": "^3.3.6",
"typescript": "5.3.3"
},
"manifest": {
"permissions": [
"storage",
"tabs"
],
"key": "$CRX_PUBLIC_KEY"
}
}
================================================
FILE: popup.tsx
================================================
// export const Popup = () => {
// return (
// <div className="w-4 h-4">
// </div>
// )
// }
================================================
FILE: postcss.config.js
================================================
/**
* @type {import('postcss').ProcessOptions}
*/
module.exports = {
plugins: {
"postcss-import": {},
"postcss-nested": {},
tailwindcss: {},
autoprefixer: {}
}
}
================================================
FILE: src/app/(app)/components/FrequentlyAskedQuestions.tsx
================================================
"use client"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion"
import BoldCopy from "@src/components/ui/bold-copy"
export const FrequentlyAskedQuestions = () => {
return (
<div>
<BoldCopy
className="border border-gray-200 dark:border-zinc-800"
text="FAQ">
</BoldCopy>
<div className="not-prose mt-4 flex flex-col gap-4 md:mt-8 text-2xl">
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">
<AccordionTrigger>1.Why was this product created?</AccordionTrigger>
<AccordionContent className="py-8 text-xl">
Our product was born out of a common workplace challenge. Many of us find ourselves juggling numerous websites throughout our workday. With an ever-growing list of URLs to remember, it's easy to get overwhelmed. This tool was developed to streamline your digital workflow and keep all your important web resources organized in one place.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>2.Do you collect or share my data?</AccordionTrigger>
<AccordionContent className="py-8 text-xl">
Your privacy is our top priority. All data is stored locally on your device. We do not have access to, collect, or share any of your personal information or browsing history. Your data remains entirely under your control.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>3. What are your future plans for the product?</AccordionTrigger>
<AccordionContent className="py-8 text-xl">
We're constantly working on improvements and new features! To stay up-to-date with our latest developments:
<li>Follow us on Twitter/X for real-time updates</li>
<li>Check our website regularly for announcements</li>
<li>Join our mailing list for exclusive news and features</li>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
)
}
================================================
FILE: src/app/(app)/components/GoogleFontSelector.tsx
================================================
import React, { useState, useEffect } from 'react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useCardStore } from '@src/hooks/useCardStore';
import { RadioGroup, RadioGroupItem } from '@components/ui/radio-group';
// import { Label } from '@radix-ui/react-select';
const googleFonts = [
{ name: 'Default', value: 'sans-serif' },
{ name: 'Roboto', value: 'Roboto' },
{ name: 'Open Sans', value: 'Open Sans' },
{ name: 'Lato', value: 'Lato' },
{ name: 'Montserrat', value: 'Montserrat' },
{ name: 'Noto Sans SC', value: 'Noto Sans SC' },
{ name: 'Playfair Display', value: 'Playfair Display' },
{ name: 'Merriweather', value: 'Merriweather' },
{ name: 'Source Sans Pro', value: 'Source Sans Pro' },
{ name: 'PT Sans', value: 'PT Sans' },
{ name: 'Raleway', value: 'Raleway' },
{ name: 'Oswald', value: 'Oswald' },
{ name: 'Nunito', value: 'Nunito' },
{ name: 'Ubuntu', value: 'Ubuntu' },
{ name: 'Poppins', value: 'Poppins' },
{ name: 'Quicksand', value: 'Quicksand' },
{ name: 'Rubik', value: 'Rubik' },
{ name: 'Work Sans', value: 'Work Sans' },
{ name: 'Fira Sans', value: 'Fira Sans' },
{ name: 'Noto Serif', value: 'Noto Serif' },
// 中文字体
{ name: 'Noto Sans SC', value: 'Noto Sans SC' },
{ name: 'Noto Serif SC', value: 'Noto Serif SC' },
{ name: 'ZCOOL XiaoWei', value: 'ZCOOL XiaoWei' },
{ name: 'ZCOOL QingKe HuangYou', value: 'ZCOOL QingKe HuangYou' },
{ name: 'Ma Shan Zheng', value: 'Ma Shan Zheng' },
];
export default function GoogleFontSelector({ onFontChange }) {
// const [selectedFont, setSelectedFont] = useState('sans-serif');
const setFontStyles = useCardStore((state) => state.updateCardStyles);
const fontFamily = useCardStore((state) => state.cardStyles.fontFamily);
const loadFont = (fontName) => {
if (fontName === 'sans-serif') return;
const link = document.createElement('link');
link.href = `https://fonts.googleapis.com/css2?family=${fontName.replace(' ', '+')}:wght@400;700&display=swap`;
link.rel = 'stylesheet';
document.head.appendChild(link);
};
useEffect(() => {
// 动态加载谷歌字体
const link = document.createElement('link');
link.href = 'https://fonts.googleapis.com/css2?family=Roboto&family=Open+Sans&family=Lato&family=Montserrat&family=Noto+Sans+SC&display=swap';
link.rel = 'stylesheet';
document.head.appendChild(link);
return () => {
document.head.removeChild(link);
};
}, []);
const handleFontChange = (value) => {
setFontStyles({
fontFamily: value
})
loadFont(value);
onFontChange?.(value);
};
return (
<div className="">
<RadioGroup value={fontFamily} onValueChange={handleFontChange}>
{googleFonts.map((font) => (
<div key={font.value} className="flex items-center space-x-2">
<RadioGroupItem value={font.value} id={font.value} />
<span>{font.name}</span>
</div>
))}
</RadioGroup>
</div>
);
}
================================================
FILE: src/app/(app)/components/ImageLayout.tsx
================================================
import { useCardStore } from '@src/hooks/useCardStore';
import React, { useEffect, useRef, useState } from 'react';
const imageCache: { [key: string]: string } = {};
const ImageLayout: React.FC<{
images: string[],
layout: 'vertical' | 'grid2' | 'grid4',
onAllImagesLoaded?: () => void
}> = ({ images, layout }) => {
if (images.length === 0) return null;
const [loadedImages, setLoadedImages] = useState<string[]>([]);
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
const loadImages = async () => {
const loadPromises = images.map(src =>
new Promise<string>(async (resolve, reject) => {
if (imageCache[src]) {
console.log('Image loaded from cache:', src);
resolve(src);
return;
}
try {
// 使用 fetch 来利用浏览器的缓存机制
const response = await fetch(src, { cache: 'force-cache' });
const blob = await response.blob();
const objectURL = URL.createObjectURL(blob);
// 将图片添加到内存缓存
imageCache[src] = objectURL;
const img = new Image();
img.src = objectURL;
img.onload = () => {
useCardStore.getState().addLoadedImage(src, 'success');
resolve(src);
}
img.onerror = () => {
useCardStore.getState().addLoadedImage(src, 'error');
reject();
};
} catch (error) {
reject(error);
useCardStore.getState().addLoadedImage(src, 'error');
}
})
);
try {
const loaded = await Promise.all(loadPromises);
if (isMounted.current) {
setLoadedImages(loaded);
}
} catch (error) {
console.error('Failed to load one or more images:', error);
}
};
loadImages();
}, [images]);
if (loadedImages.length === 0) return <div>Loading...</div>;
const renderImage = (src: string, index: number, className: string = "w-full h-full object-contain") => (
<img
key={index}
src={imageCache[src] || src}
alt={`Image ${index + 1}`}
className={className}
/>
);
const layoutStyles = {
vertical: "flex flex-col space-y-2",
grid2: "grid grid-cols-2 gap-2",
grid4: "grid grid-cols-2 grid-rows-2 gap-2"
};
if (images.length === 2) {
return (
<div className="w-full flex flex-row gap-2 ">
<div className="w-1/2">
{renderImage(images[0], 0)}
</div>
<div className="w-1/2">
{renderImage(images[1], 1)}
</div>
</div>
);
}
if (images.length === 3) {
return (
<div className="w-full flex flex-row gap-2 ">
<div className="w-1/2">
{renderImage(images[0], 0)}
</div>
<div className="w-1/4 flex flex-col gap-2">
<div className="h-1/2">
{renderImage(images[1], 1)}
</div>
<div className="h-1/2">
{renderImage(images[2], 2)}
</div>
</div>
</div>
);
}
return (
<div className={`w-full ${layoutStyles[layout]}`}>
{images.slice(0, layout === 'vertical' ? undefined : (layout === 'grid2' ? 2 : 4))
.map((src, index) => renderImage(src, index))}
</div>
);
};
export default ImageLayout;
================================================
FILE: src/app/(app)/components/LazyLoadAnimatedSection.tsx
================================================
import { useInView } from 'react-intersection-observer';
import { motion} from "framer-motion"
const LazyLoadAnimatedSection = ({ children, animation = 'fadeIn' }) => {
const [ref, inView] = useInView({
triggerOnce: true,
threshold: 0.1, // Adjust this value to control when the animation triggers
});
const animations = {
fadeIn: {
opacity: inView ? 1 : 0,
y: inView ? 0 : 50,
transition: { duration: 0.5 }
},
slideIn: {
x: inView ? 0 : -100,
opacity: inView ? 1 : 0,
transition: { duration: 0.5 }
},
scaleIn: {
scale: inView ? 1 : 0.8,
opacity: inView ? 1 : 0,
transition: { duration: 0.5 }
}
};
return (
<motion.div
ref={ref}
initial={false}
animate={animations[animation]}
>
{children}
</motion.div>
);
};
export default LazyLoadAnimatedSection;
================================================
FILE: src/app/(app)/components/ResultIcon.tsx
================================================
import React, { useId } from "react";
// import noisePicture from "../assets/noise.inline.png";
// import { SettingsType } from "../lib/types";
type PropTypes = {
// settings: SettingsType;
size?: number;
isPreview?: boolean;
// TODO: fix icon type?
IconComponent?: React.FC<React.SVGProps<SVGSVGElement>>;
};
const ResultIcon = React.forwardRef<SVGSVGElement, PropTypes>(
({ settings, size = 512, isPreview, IconComponent }, svgRef) => {
const strokeSize = isPreview ? 0 : settings.backgroundStrokeSize;
const strokeWidth = isNaN(parseInt(strokeSize.toString())) ? 0 : parseInt(strokeSize.toString());
const rectId = useId().replace(/:/g, "");
const gradientId = useId().replace(/:/g, "");
const radialGlareGradientId = useId().replace(/:/g, "");
const gradientX = settings.backgroundPosition?.split(",")[0];
const gradientY = settings.backgroundPosition?.split(",")[1];
return (
<svg
ref={svgRef}
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<rect
id={rectId}
width={size - strokeSize}
height={size - strokeSize}
x={strokeSize / 2}
y={strokeSize / 2}
rx={settings.backgroundRadius}
fill={settings.backgroundFillType === "Solid" ? settings.backgroundStartColor : `url(#${gradientId})`}
stroke={settings.backgroundStrokeColor}
strokeWidth={strokeWidth}
strokeOpacity={`${settings.backgroundStrokeOpacity}%`}
paintOrder="stroke"
/>
{settings.backgroundRadialGlare ? (
<rect
width={size - strokeSize}
height={size - strokeSize}
x={strokeSize / 2}
y={strokeSize / 2}
fill={`url(#${radialGlareGradientId})`}
rx={settings.backgroundRadius}
style={{ mixBlendMode: "overlay" }}
/>
) : null}
{settings.backgroundNoiseTexture && !isPreview ? (
<image
// href={noisePicture as unknown as string}
width={size - strokeSize}
height={size - strokeSize}
x={strokeSize / 2}
y={strokeSize / 2}
clipPath="url(#clip)"
opacity={`${settings.backgroundNoiseTextureOpacity}%`}
/>
) : null}
<clipPath id="clip">
<use xlinkHref={`#${rectId}`} />
</clipPath>
<defs>
{settings.backgroundFillType === "Radial" ? (
<radialGradient
id={gradientId}
cx="50%"
cy="50%"
r="100%"
fx={gradientX}
fy={gradientY}
gradientUnits="objectBoundingBox"
>
<stop stopColor={settings.backgroundStartColor} />
<stop offset={settings.backgroundSpread / 100} stopColor={settings.backgroundEndColor} />
</radialGradient>
) : (
<linearGradient
id={gradientId}
gradientUnits="userSpaceOnUse"
gradientTransform={`rotate(${settings.backgroundAngle})`}
style={{ transformOrigin: "center" }}
>
<stop stopColor={settings.backgroundStartColor} />
<stop offset="1" stopColor={settings.backgroundEndColor} />
</linearGradient>
)}
<radialGradient
id={radialGlareGradientId}
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform={`translate(${size / 2}) rotate(90) scale(${size})`}
>
<stop stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</radialGradient>
</defs>
{IconComponent ? (
<IconComponent
width={settings.iconSize}
height={settings.iconSize}
x={(size - settings.iconSize) / 2 + +settings.iconOffsetX}
y={(size - settings.iconSize) / 2 + +settings.iconOffsetY}
style={{ color: settings.iconColor }}
alignmentBaseline="middle"
/>
) : null}
</svg>
);
}
);
ResultIcon.displayName = "ResultIcon";
export default ResultIcon;
================================================
FILE: src/app/(app)/components/card-generator/color.tsx
================================================
import { cn } from "@lib/utils";
import { ColorChangeHandler, SketchPicker } from "react-color";
import ResultIcon from "../ResultIcon";
import { AccordionContent, AccordionTrigger, AccordionItem } from "@components/ui/accordion";
import { useCardStore } from "@src/hooks/useCardStore";
import { ColorPicker } from "@components/ui/color-picker";
export const presets: PresetType[] = [
{
backgroundFillType: "Linear",
backgroundStartColor: "#FF7DB4",
backgroundEndColor: "#654EA3",
backgroundAngle: 45,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#8E2DE2",
backgroundEndColor: "#4A00E0",
backgroundAngle: 45,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#99F2C8",
backgroundEndColor: "#1F4037",
backgroundAngle: 45,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#F953C6",
backgroundEndColor: "#B91D73",
backgroundAngle: 45,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#91EAE4",
backgroundEndColor: "#7F7FD5",
backgroundAngle: 45,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#F5AF19",
backgroundEndColor: "#F12711",
backgroundAngle: 45,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#EAAFC8",
backgroundEndColor: "#EC2F4B",
backgroundAngle: 45,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#00B4DB",
backgroundEndColor: "#003357",
backgroundAngle: 45,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#A8C0FF",
backgroundEndColor: "#3F2B96",
backgroundAngle: 90,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#DD1818",
backgroundEndColor: "#380202",
backgroundAngle: 135,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#DECBA4",
backgroundEndColor: "#3E5151",
backgroundAngle: 45,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#FC466B",
backgroundEndColor: "#3F5EFB",
backgroundAngle: 180,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#CCCFE2",
backgroundEndColor: "#25242B",
backgroundAngle: 180,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#68AEFF",
backgroundEndColor: "#003EB7",
backgroundAngle: 180,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#C9D6FF",
backgroundEndColor: "#596AA1",
backgroundAngle: 180,
},
{
backgroundFillType: "Linear",
backgroundStartColor: "#5C5C5C",
backgroundEndColor: "#0F1015",
backgroundAngle: 180,
},
{
backgroundFillType: "Radial",
backgroundStartColor: "#695BF8",
backgroundEndColor: "#131308",
backgroundPosition: "50%,0%",
},
{
backgroundFillType: "Radial",
backgroundStartColor: "#4d4d4d",
backgroundEndColor: "#000000",
backgroundPosition: "50%,0%",
},
{
backgroundFillType: "Radial",
backgroundStartColor: "#f5af19",
backgroundEndColor: "#f12711",
backgroundPosition: "50%,50%",
},
{
backgroundFillType: "Radial",
backgroundStartColor: "#1D6E47",
backgroundEndColor: "#041B11",
backgroundPosition: "50%,0%",
},
{
backgroundFillType: "Radial",
backgroundStartColor: "#ffffff",
backgroundEndColor: "#666666",
backgroundPosition: "50%,100%",
},
{
backgroundFillType: "Radial",
backgroundStartColor: "#d9f1f8",
backgroundEndColor: "#002069",
backgroundPosition: "50%,100%",
},
{
backgroundFillType: "Radial",
backgroundStartColor: "#f95356",
backgroundEndColor: "#7e0000",
backgroundPosition: "50%,50%",
},
{
backgroundFillType: "Radial",
backgroundStartColor: "#ffbb00",
backgroundEndColor: "#ffe74b",
backgroundPosition: "50%,0%",
},
];
type ColorInputPropTypes = {
value: string;
name: string;
recentColors: string[];
onChange: ColorChangeHandler;
disabled?: boolean;
};
export const ColorSelect = () => {
const colorIndex = useCardStore(state => state.colorIndex);
const setColorIndex = useCardStore(state => state.setColorIndex);
const updateCardStyles = useCardStore((state) => state.updateCardStyles);
// const filStyles = useCardStore(state => state.filStyles);
return (
<div className={cn('flex flex-wrap gap-4 px-2 py-2')}>
{presets.map((preset, index) => {
return (
<label key={index}
className={cn(
'relative overflow-hidden w-5 h-5 shrink-0 rounded-[5px] ',
colorIndex === index ? "dark:ring-primary-500 dark:ring-opacity-100 dark:ring-2 dark:ring-offset-2 dark:ring-offset-gray-700 ring-primary-500 ring-2 ring-offset-2 ring-offset-white inline-flex active:scale-95 transition " : ""
)}
>
<input
className={cn(
"absolute w-full h-full appearance-none opacity-0 inset-0 cursor-pointer",
)}
type="radio"
name="preset"
value={index}
checked={colorIndex === index}
onChange={() => {
setColorIndex(index);
}}
/>
<ResultIcon size={20} isPreview settings={{ ...preset, backgroundRadius: 0 }} />
</label>
);
})}
{/* <ColorPicker className=" w-[20px] h-[20px]" value={cardStyles.borderColor}
onChange={(v) => {
updateCardStyles({
borderColor: v,
});
}}
></ColorPicker> */}
</div>
)
}
================================================
FILE: src/app/(app)/components/card-generator/controller/background-controller.tsx
================================================
import { AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion";
import { ColorPicker } from "@components/ui/color-picker";
import { ColorSelect } from "../color";
import { Switch } from "@components/ui/switch";
import { useCardStore } from "@src/hooks/useCardStore";
import { Slider } from "@components/ui/slider";
import { Input } from "@components/ui/input";
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
export const BackgroundController = (props) => {
const tabConfig = useCardStore((state) => state.tabConfig);
const setTabConfig = useCardStore((state) => state.setTabConfig);
const backgroundStyles = useCardStore((state) => state.backgroundStyles);
const updateBackgroundStyles = useCardStore((state) => state.updateBackgroundStyles);
const handleImageUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => updateBackgroundStyles({
backgroundImage: e.target.result as string,
})
reader.readAsDataURL(file);
}
};
return (
<AccordionItem value={'background'}>
<AccordionTrigger>Background</AccordionTrigger>
<AccordionContent>
{/* preset color */}
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">Preset Color</span>
<div className=" w-full">
<ColorSelect></ColorSelect>
</div>
</label>
{/* custom Color */}
<div>
<label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className="grow-[2] text-[13px]">Custom Color</span>
<div className="inline-flex">
<Switch checked={tabConfig.openCustomColor}
onCheckedChange={v => setTabConfig({ openCustomColor: v })}
/>
</div>
</label>
<AnimatePresence>
{tabConfig?.openCustomColor && <motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<div className="py-2">
<ColorPicker className=" w-[20px] h-[20px]" value={backgroundStyles.backgroundColor}
onChange={(v) => {
updateBackgroundStyles({
backgroundColor: v,
});
}}
></ColorPicker>
</div>
{/* Background Opacity */}
<label className="flex flex-col gap-y-2">
<span className="text-[13px]">Background Opacity</span>
<div className="py-2">
<Slider
min={0}
max={1}
step={0.1}
value={[backgroundStyles.backgroundOpacity]}
onValueChange={(value) => {
updateBackgroundStyles({
backgroundOpacity: value,
})
}}
/>
</div>
</label>
{/* Background Blur */}
<label className="flex flex-col gap-y-2">
<span className="text-[13px]">Background Blur</span>
<div className="py-2">
<Slider
min={0}
max={20}
step={1}
value={[backgroundStyles.backgroundBlur]}
onValueChange={(value) => updateBackgroundStyles({
backgroundBlur: value,
})}
/>
</div>
</label>
</motion.div>}
</AnimatePresence>
{/* Background Gradient */}
<label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className="grow-[2] text-[13px]">Use Gradient</span>
<div className="inline-flex">
<Switch
checked={backgroundStyles.useGradient}
onCheckedChange={(v) => updateBackgroundStyles({ useGradient: v })}
/>
</div>
</label>
<AnimatePresence>
{backgroundStyles.useGradient && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<label className="flex flex-col gap-y-2">
<span className="text-[13px]">Gradient Angle</span>
<div className="py-2">
<Slider
min={0}
max={360}
step={1}
value={[backgroundStyles.backgroundGradientAngle]}
onValueChange={(value) => updateBackgroundStyles({ backgroundGradientAngle: value[0] })}
/>
</div>
</label>
<label className="flex flex-col gap-y-2">
<span className="text-[13px]">Gradient Start Color</span>
<ColorPicker
value={backgroundStyles.backgroundStartColor}
onChange={(v) => updateBackgroundStyles({ backgroundStartColor: v })}
/>
</label>
<label className="flex flex-col gap-y-2">
<span className="text-[13px]">Gradient End Color</span>
<ColorPicker
value={backgroundStyles.backgroundEndColor}
onChange={(v) => updateBackgroundStyles({ backgroundEndColor: v })}
/>
</label>
</motion.div>
)}
</AnimatePresence>
</div>
{/* preset Image */}
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className="text-[13px]">Image</span>
<div className="w-full">
<Input
onChange={handleImageUpload}
accept="image/*"
type="file"
multiple
/>
</div>
</label>
{/* <label className="flex flex-col gap-y-2">
<span className="text-[13px]">Background Position</span>
<Select
value={backgroundStyles.backgroundPosition}
onValueChange={(value) => updateBackgroundStyles({ backgroundPosition: value })}
>
<Select.Option value="center center">Center</Select.Option>
<Select.Option value="top left">Top Left</Select.Option>
<Select.Option value="top right">Top Right</Select.Option>
<Select.Option value="bottom left">Bottom Left</Select.Option>
<Select.Option value="bottom right">Bottom Right</Select.Option>
</Select>
</label> */}
{/* Background Repeat */}
{/* <label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className="grow-[2] text-[13px]">Background Repeat</span>
<div className="inline-flex">
<Switch
checked={backgroundStyles.backgroundRepeat === 'repeat'}
onCheckedChange={(v) => updateBackgroundStyles({ backgroundRepeat: v ? 'repeat' : 'no-repeat' })}
/>
</div>
</label> */}
{/* Background Width */}
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">Width</span>
<div className=" w-full">
<Slider step={1}
value={[backgroundStyles.backgroundWidth]}
max={100}
min={50}
onValueChange={(v) => {
updateBackgroundStyles({
backgroundWidth: v[0],
});
}}
></Slider>
</div>
</label>
{/* background padding */}
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">Padding</span>
<div className=" w-full">
<Slider step={1}
value={[backgroundStyles.padding]}
max={100}
min={0}
onValueChange={(v) => {
updateBackgroundStyles({
padding: v[0],
});
}}
></Slider>
</div>
</label>
{/* <label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className="grow-[2] text-[13px]">Noise</span>
<div className="inline-flex">
<Switch checked={cardStyles.hasNoiseTexture}
onCheckedChange={v => updateCardStyles({ hasNoiseTexture: v })}
/>
</div>
</label>
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">Opacity</span>
<div className=" w-full">
<Slider step={0.01}
value={[cardStyles.noiseTextureOpacity]}
max={1}
min={0}
onValueChange={(v) => {
console.log('v', v);
updateCardStyles({
noiseTextureOpacity: v[0],
});
}}
></Slider>
</div>
</label>
<label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className="grow-[2] text-[13px]">Noise</span>
<SelectBackgroundPosition onChange={v => {
updateCardStyles({
texturePosition: v,
});
}}></SelectBackgroundPosition>
</label> */}
</AccordionContent>
</AccordionItem>
)
}
================================================
FILE: src/app/(app)/components/card-generator/controller/card-controller.tsx
================================================
import { AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion";
import { ColorPicker } from "@components/ui/color-picker";
import { Slider } from "@components/ui/slider";
import { useCardStore } from "@src/hooks/useCardStore";
import { useState } from "react";
import { RadioGroup, RadioGroupItem } from "@components/ui/radio-group";
import LayoutOptions from "@src/components/extension/layout-options";
export const CardController = () => {
const cardStyles = useCardStore((state) => state.cardStyles);
const updateCardStyles = useCardStore((state) => state.updateCardStyles);
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const handleImageUpload = (event) => {
// const file = event.target.files[0];
// if (file) {
// const reader = new FileReader();
// reader.onload = (e) => updateBackgroundStyles({
// backgroundImage: e.target.result as string,
// })
// reader.readAsDataURL(file);
// }
};
const handleLayoutChange = () => {
}
return (
<AccordionItem value={'card'}>
<AccordionTrigger>Card</AccordionTrigger>
<AccordionContent className="">
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">Width</span>
<div className=" w-full">
<Slider step={1}
value={[cardStyles.width]}
max={1080}
min={379}
onValueChange={(v) => {
updateCardStyles({
width: v[0],
});
}}
></Slider>
</div>
</label>
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]"> Scale</span>
<div className=" w-full">
<Slider step={1}
value={[cardStyles.scale]}
max={150}
min={1}
onValueChange={(v) => {
console.log('v', v);
updateCardStyles({
scale: v[0],
});
}}
></Slider>
</div>
</label>
<label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className="grow-[2] text-[13px]">Border Width</span>
<div className=" w-full">
<Slider step={1}
value={[cardStyles.borderWidth]}
max={10}
min={0}
onValueChange={(v) => {
updateCardStyles({
borderWidth: v[0],
});
}}
></Slider>
</div>
</label>
<label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className="grow-[2] text-[13px]">Border Color</span>
<ColorPicker className=" w-[20px] h-[20px]" value={cardStyles.borderColor}
onChange={(v) => {
updateCardStyles({
borderColor: v,
});
}}
></ColorPicker>
</label>
<label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className="grow-[2] text-[13px]">Style</span>
<RadioGroup value={cardStyles.style} onValueChange={(v) => {
updateCardStyles({
style: v,
});
}}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="article" id="article" />
<span>article</span>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="posts" id="posts" />
<span>posts</span>
</div>
</RadioGroup>
</label>
<label className="flex min-h-[40px] flex-row items-center justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className="grow-[2] text-[13px]">Border Radius</span>
<Slider step={1}
value={[cardStyles.borderRadius]}
min={0}
onValueChange={(v) => {
updateCardStyles({
borderRadius: v[0],
});
}}
></Slider>
</label>
{/* <label className="flex flex-col min-h-[40px] justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className="grow-[2] text-[13px]">Resize</span>
<LayoutOptions onSelect={(option) => {
updateCardStyles({
width: option.dimensions.width,
height: option.dimensions.height,
})
}}></LayoutOptions>
</label> */}
{selectedFiles.length > 0 && (
<div className="mt-2">
<p className="text-sm mb-1">Selected files:</p>
<ul className="list-disc list-inside">
{selectedFiles.map((file, index) => (
<li key={index} className="text-sm">{file.name}</li>
))}
</ul>
</div>
)}
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className="text-[13px]">Image Layout</span>
<div className="w-full">
<RadioGroup value={cardStyles.imageLayout} onValueChange={(v) => {
updateCardStyles({
imageLayout: v,
})
}}>
{[
'vertical',
'grid2',
'grid4',
].map((layout) => (
<div key={layout} className="flex items-center space-x-2">
<RadioGroupItem value={layout} id={layout} />
<span>{layout}</span>
</div>
))}
</RadioGroup>
</div>
</label>
</AccordionContent>
</AccordionItem>
)
}
================================================
FILE: src/app/(app)/components/card-generator/controller/font-controller.tsx
================================================
import { AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion";
import { Slider } from "@components/ui/slider";
import { useCardStore } from "@src/hooks/useCardStore";
import GoogleFontSelector from "../../GoogleFontSelector";
interface FontControllerProps {
}
const googleFonts = [
{ name: 'Default', value: 'sans-serif' },
{ name: 'Roboto', value: "'Roboto', sans-serif" },
{ name: 'Open Sans', value: "'Open Sans', sans-serif" },
{ name: 'Lato', value: "'Lato', sans-serif" },
{ name: 'Montserrat', value: "'Montserrat', sans-serif" },
{ name: 'Noto Sans SC', value: "'Noto Sans SC', sans-serif" }, // 中文字体
];
export const FontController = (props: FontControllerProps) => {
const updateCardStyles = useCardStore((state) => state.updateCardStyles);
const cardStyles = useCardStore((state) => state.cardStyles);
return (
<AccordionItem value={'Font'}>
<AccordionTrigger>Font</AccordionTrigger>
<AccordionContent className="">
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">font-size</span>
<div className=" w-full">
<Slider step={0.5}
value={[cardStyles.fontSize]}
max={24}
min={12}
onValueChange={(v) => {
updateCardStyles({
fontSize: v[0],
});
}}
></Slider>
</div>
</label>
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">Select Font</span>
<div className=" w-full">
<GoogleFontSelector></GoogleFontSelector>
</div>
</label>
</AccordionContent>
</AccordionItem>
)
}
================================================
FILE: src/app/(app)/components/card-generator/controller/iframe-controller.tsx
================================================
{/* <AccordionItem value={'frames'}>
<AccordionTrigger>Frames</AccordionTrigger>
<AccordionContent>
<div className="py-2">
<div className=" max-w-xl rounded-xl flex flex-col">
<div className="max-h-96 overflow-y-auto">
<div className=" grid grid-cols-3 gap-4 items-center p-4">
<button
name="None"
className="flex flex-col gap-1 justify-center items-center w-10 outline-none text-gray transition-all ease-in-out active:scale-95 "
>
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth={0}
viewBox="0 0 16 16"
className="w-10 h-10"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M2.5 5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1ZM4 5a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1Zm2-.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0Z" />
<path d="M0 4a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v4a.5.5 0 0 1-1 0V7H1v5a1 1 0 0 0 1 1h5.5a.5.5 0 0 1 0 1H2a2 2 0 0 1-2-2V4Zm1 2h13V4a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v2Z" />
<path d="M16 12.5a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0Zm-4.854-1.354a.5.5 0 0 0 0 .708l.647.646-.647.646a.5.5 0 0 0 .708.708l.646-.647.646.647a.5.5 0 0 0 .708-.708l-.647-.646.647-.646a.5.5 0 0 0-.708-.708l-.646.647-.646-.647a.5.5 0 0 0-.708 0Z" />
</svg>
<span className="text-sm">None</span>
</button>
<button
name="MacOS"
className="flex flex-col gap-1 justify-center items-center w-10 outline-none text-gray transition-all ease-in-out active:scale-95 "
>
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth={0}
viewBox="0 0 16 16"
className="w-10 h-10"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.386 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z" />
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516.024.034 1.52.087 2.475-1.258.955-1.345.762-2.391.728-2.43Zm3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422.212-2.189 1.675-2.789 1.698-2.854.023-.065-.597-.79-1.254-1.157a3.692 3.692 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56.244.729.625 1.924 1.273 2.796.576.984 1.34 1.667 1.659 1.899.319.232 1.219.386 1.843.067.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758.347-.79.505-1.217.473-1.282Z" />
</svg>
<span className="text-sm">MacOS</span>
</button>
<button
name="Windows"
className="flex flex-col gap-1 justify-center items-center w-10 outline-none text-gray transition-all ease-in-out active:scale-95 "
>
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth={0}
viewBox="0 0 16 16"
className="w-10 h-10"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M6.555 1.375 0 2.237v5.45h6.555V1.375zM0 13.795l6.555.933V8.313H0v5.482zm7.278-5.4.026 6.378L16 16V8.395H7.278zM16 0 7.33 1.244v6.414H16V0z" />
</svg>
<span className="text-sm">Windows</span>
</button>
<button
name="Fimojis"
className="flex flex-col gap-1 justify-center items-center w-10 outline-none text-gray transition-all ease-in-out active:scale-95 "
>
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth={0}
viewBox="0 0 16 16"
className="w-10 h-10"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M4.968 9.75a.5.5 0 1 0-.866.5A4.498 4.498 0 0 0 8 12.5a4.5 4.5 0 0 0 3.898-2.25.5.5 0 1 0-.866-.5A3.498 3.498 0 0 1 8 11.5a3.498 3.498 0 0 1-3.032-1.75zM7 5.116V5a1 1 0 0 0-1-1H3.28a1 1 0 0 0-.97 1.243l.311 1.242A2 2 0 0 0 4.561 8H5a2 2 0 0 0 1.994-1.839A2.99 2.99 0 0 1 8 6c.393 0 .74.064 1.006.161A2 2 0 0 0 11 8h.438a2 2 0 0 0 1.94-1.515l.311-1.242A1 1 0 0 0 12.72 4H10a1 1 0 0 0-1 1v.116A4.22 4.22 0 0 0 8 5c-.35 0-.69.04-1 .116z" />
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-1 0A7 7 0 1 0 1 8a7 7 0 0 0 14 0z" />
</svg>
<span className="text-sm">Fimojis</span>
</button>
<button
name="Emojis"
className="flex flex-col gap-1 justify-center items-center w-10 outline-none text-gray transition-all ease-in-out active:scale-95 "
>
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth={0}
viewBox="0 0 16 16"
className="w-10 h-10"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8 16c3.314 0 6-2 6-5.5 0-1.5-.5-4-2.5-6 .25 1.5-1.25 2-1.25 2C11 4 9 .5 6 0c.357 2 .5 4-2 6-1.25 1-2 2.729-2 4.5C2 14 4.686 16 8 16Zm0-1c-1.657 0-3-1-3-2.75 0-.75.25-2 1.25-3C6.125 10 7 10.5 7 10.5c-.375-1.25.5-3.25 2-3.5-.179 1-.25 2 1 3 .625.5 1 1.364 1 2.25C11 14 9.657 15 8 15Z" />
</svg>
<span className="text-sm">Emojis</span>
</button>
</div>
</div>
</div>
</div>
</AccordionContent>
</AccordionItem> */}
================================================
FILE: src/app/(app)/components/card-generator/controller/input-controller.tsx
================================================
import { AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion";
import { Input } from "@components/ui/input";
import { Textarea } from "@components/ui/textarea";
import { useCardStore } from "@src/hooks/useCardStore";
export const InputController = () => {
const xConfig = useCardStore(state => state.xConfig);
return (
<div>
<AccordionItem value={'Input'}>
<AccordionTrigger>Input</AccordionTrigger>
<AccordionContent className="">
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">username</span>
<div className=" px-1.5">
<Input maxLength={200} minLength={2} value={xConfig.username} onChange={(e) => {
useCardStore.setState({
xConfig: {
...xConfig,
username: e.target.value
}
})
}}></Input>
</div>
</label>
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">text</span>
<div className=" px-1.5">
<Textarea rows={10} value={xConfig.text} onChange={(e) => {
useCardStore.setState({
xConfig: {
...xConfig,
text: e.target.value
}
})
}}></Textarea>
</div>
</label>
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">replies</span>
<div className=" px-1.5">
<Input type="number" value={xConfig.replies} onChange={(e) => {
useCardStore.setState({
xConfig: {
...xConfig,
replies: Number(e.target.value)
}
})
}}></Input>
</div>
</label>
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">shares</span>
<div className=" px-1.5">
<Input type="number" value={xConfig.shares} onChange={(e) => {
useCardStore.setState({
xConfig: {
...xConfig,
shares: Number(e.target.value)
}
})
}}></Input>
</div>
</label>
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">shares</span>
<div className=" px-1.5">
<Input type="number" value={xConfig.likes} onChange={(e) => {
useCardStore.setState({
xConfig: {
...xConfig,
likes: Number(e.target.value)
}
})
}}></Input>
</div>
</label>
</AccordionContent>
</AccordionItem>
</div>
)
}
================================================
FILE: src/app/(app)/components/card-generator/display.tsx
================================================
import { CommonLayouts, useCardStore } from "@src/hooks/useCardStore"
import { useMemo } from "react"
import { presets } from "./color"
import { TwitterCard } from "./twitter-card"
import { WeChatCard } from "./wechat-card"
export const Display = () => {
const colorIndex = useCardStore((state) => state.colorIndex)
const cardStyles = useCardStore((state) => state.cardStyles)
const tabConfig = useCardStore((state) => state.tabConfig);
const backgroundStyles = useCardStore((state) => state.backgroundStyles)
const xConfig = useCardStore((state) => state.xConfig)
const finalBackgroundStyles = useMemo(() => {
const color = presets[colorIndex]
let borderRadius = cardStyles.borderRadius;
if (backgroundStyles.padding === 0) {
borderRadius = 0;
}
if (backgroundStyles?.useGradient) {
return {
...backgroundStyles,
borderRadius,
backgroundImage: `linear-gradient(${backgroundStyles.backgroundGradientAngle}deg, ${backgroundStyles.backgroundStartColor}, ${backgroundStyles.backgroundEndColor})`,
}
}
if (tabConfig.openCustomColor) {
return {
...backgroundStyles,
borderRadius,
backgroundColor: backgroundStyles.backgroundColor,
}
}
if (color.backgroundFillType === "Linear") {
return {
...backgroundStyles,
borderRadius,
backgroundImage: `linear-gradient(${color.backgroundAngle}deg, ${color.backgroundStartColor}, ${color.backgroundEndColor})`,
backgroundRepeat: "no-repeat",
}
} else if (color.backgroundFillType === "Radial") {
return {
...backgroundStyles,
borderRadius,
backgroundImage: `radial-gradient(${color.backgroundStartColor}, ${color.backgroundEndColor})`,
backgroundRepeat: "no-repeat",
backgroundPosition: color.backgroundPosition,
}
} else {
return {
...backgroundStyles,
borderRadius,
}
}
}, [colorIndex, backgroundStyles, tabConfig.openCustomColor])
const calculateScale = (width, height) => {
const maxWidth = 1920 / 2; // Maximum width of the card in the display
const maxHeight = 1080 / 2; // Maximum height of the card in the display
const widthScale = maxWidth / width;
const heightScale = maxHeight / height;
return Math.min(widthScale, heightScale);
};
return (
<div
className="w-full justify-center flex gap-x-4 relative mt-0">
<TwitterCard
xConfig={xConfig}
cardStyles={{
...cardStyles,
// aspectRatio: "16/9",
// width: '1920px',
// height: '1080px',
}}
backgroundStyles={finalBackgroundStyles}
></TwitterCard>
{/* <WeChatCard xConfig={xConfig}></WeChatCard> */}
</div>
)
}
================================================
FILE: src/app/(app)/components/card-generator/export-tab.tsx
================================================
import { AccordionContent, AccordionItem, AccordionTrigger } from "@components/ui/accordion";
import { Button } from "@components/ui/button";
import { exportImage } from "@src/app/utils/export";
export const ExportTab = () => {
return (
<AccordionItem value={'export'} className=" border border-destructive ">
<AccordionTrigger className="">Export </AccordionTrigger>
<AccordionContent className=" ">
<label className="flex min-h-[40px] flex-col gap-y-2 justify-start transition-opacity duration-[0.15s] ease-[ease-in-out] py-1">
<span className=" text-[13px]">image type</span>
<div className="mt-4 flex space-x-2">
<Button className="" size="sm" onClick={() => exportImage('png')}>As PNG</Button>
<Button size="sm" onClick={() => exportImage('jpeg')}>As JPEG</Button>
<Button size="sm" onClick={() => exportImage('svg')}>As SVG</Button>
</div>
</label>
</AccordionContent>
</AccordionItem>
)
}
================================================
FILE: src/app/(app)/components/card-generator/index.tsx
================================================
import { Accordion, } from "@components/ui/accordion";
import { Display } from "./display";
import { useCardStore } from "@src/hooks/useCardStore";
import { BackgroundController } from "./controller/background-controller";
import { FontController } from "./controller/font-controller";
import { ExportTab } from "./export-tab";
import { Button } from "@components/ui/button";
import { InputController } from "./controller/input-controller";
import { SaveAsTemplateButton } from "../save-as-template-button";
import { CardController } from "./controller/card-controller";
export const CardGenerator = () => {
const resetAll = useCardStore((state) => state.resetAll);
return (
<div className="flex mx-auto w-full md:py-5 px-0 md:px-6">
<Display></Display>
<div className="flex-1 min-w-[320px] h-[578px] px-4 pb-4 overflow-auto sm:block hidden ">
<div className="py-2 flex gap-x-2">
<Button onClick={() => resetAll()} size="sm" variant="secondary">Reset</Button>
<SaveAsTemplateButton></SaveAsTemplateButton>
</div>
<Accordion type="multiple" className="w-full">
<ExportTab></ExportTab>
<InputController></InputController>
<BackgroundController></BackgroundController>
<CardController></CardController>
<FontController></FontController>
</Accordion>
</div>
</div >
)
}
================================================
FILE: src/app/(app)/components/card-generator/twitter-card.tsx
================================================
import React, { useMemo, useState } from 'react';
import { cn } from '@/lib/utils'; // Assuming you have a utility for class names
import ImageLayout from '../ImageLayout';
import XLogo from '@assets/x-logo.svg';
import * as _ from "lodash-es"
import { formatTimestamp } from '@src/app/utils/format';
import type { CardStore, XConfig } from '@src/hooks/useCardStore';
interface TwitterCardProps {
xConfig: XConfig[],
backgroundStyles: CardStore['backgroundStyles'],
cardStyles: CardStore['cardStyles'],
}
export const TwitterCard: React.FC<TwitterCardProps> = ({ xConfig, backgroundStyles, cardStyles }) => {
const card = useMemo(() => {
const controls = cardStyles.controls;
return (
<div className='flex flex-col gap-y-4 relative'>
{
xConfig.map((config, index) => (
<div className="flex flex-col h-full" key={`xConfig-${index}`}>
<CardHeader xConfig={config} controls={controls} />
<CardBody xConfig={config} cardStyles={cardStyles} />
{controls.showFooter && (<CardFooter xConfig={config} />)}
</div>
))
}
<div className=' absolute right-0 bottom-0 opacity-40 text-[##6d6d6d]'>
∙ Made with x-cards.net
</div>
</div>
)
}, [backgroundStyles, xConfig, cardStyles]);
return (
<div
id="card"
// aspect-video
className="flex h-fit"
style={{
boxShadow: 'rgba(245, 208, 254, 0.3) 0px 0px 200px',
width: cardStyles.width,
}}
>
<div className="relative w-full grid place-items-center mobile-scaling pointer-events-none">
<div className=" overflow-hidden w-full h-full relative content-shadow">
<div
id="content"
className="grid place-items-center content-container transition-colors h-full w-full"
style={{
padding: backgroundStyles.padding || 0,
fontSize: 14,
perspective: 1000
}}
>
{/* Background layers */}
<BackgroundLayers backgroundStyles={backgroundStyles} cardStyles={cardStyles} />
{/* Card container */}
<div
className="relative z-20 transition-all w-full card-holder"
style={{
transform: `scale(${cardStyles.scale}%)`,
// pointer-events: none;
// fontFamily: cardStyles?.fontFamily && `'${cardStyles?.fontFamily}', sans-serif`,
// fontFamily: 'ui-sans-serif',
}}
>
{/* Card body */}
<div
className={cn(
"select-none relative transition-all",
"h-full w-full backdrop-blur-[18px] backdrop-saturate-[177%] pt-[2em] pb-[1.5em] px-[2em]",
'card-background-light transition-colors inset-0 rounded-[inherit]',
// absolute
)}
style={{
overflow: 'visible',
zIndex: -1,
background: `linear-gradient(150deg, rgba(255,255,255,0.5), rgba(255,255,255,0.95) 80%)`,
// boxShadow: 'inset 0 0 0 2px rgba(255,255,255,0.15)',
borderRadius: `${backgroundStyles.borderRadius}px`,
}}
>
{card}
</div>
</div>
</div>
</div>
</div>
</div >
);
};
const BackgroundLayers = ({ backgroundStyles, cardStyles }) => (
<>
<div
className="absolute inset-0"
style={{
..._.omit(backgroundStyles, 'borderRadius'),
backgroundRepeat: "no-repeat",
}}
/>
{backgroundStyles?.backgroundImage && (
<div
className="absolute inset-0"
style={{
backgroundRepeat: backgroundStyles.backgroundRepeat,
backgroundImage: `url(${backgroundStyles.backgroundImage})`,
filter: `blur(${backgroundStyles.backgroundBlur}px)`,
opacity: backgroundStyles.backgroundOpacity,
}}
/>
)}
{cardStyles.hasNoiseTexture && (
<div
className="absolute inset-0 mix-blend-overlay"
style={{
backgroundImage: `url(/noise1.png)`, // Assuming noise1 is now a public asset
backgroundRepeat: "repeat",
opacity: cardStyles.noiseTextureOpacity,
backgroundPosition: cardStyles.texturePosition,
zIndex: 10,
borderRadius: `${cardStyles.borderRadius}px`,
}}
/>
)}
</>
);
const CardHeader: React.FC<{
xConfig: XConfig,
controls: CardStore['cardStyles']['controls'],
}> = ({ xConfig, controls }) => {
return (
<div className='flex w-full'>
{controls.showUser ? (<div className="flex flex-grow items-center pb-3">
<img
src={xConfig?.avatar || ''}
className="inline object-cover rounded-full transition-all duration-150"
alt="Profile image"
style={{
width: "3em",
height: "3em",
marginRight: "0.75em"
}}
/>
<div>
<div className="flex text-secondary-foreground" style={{ fontWeight: 600, lineHeight: "1.2" }}>
<div className="whitespace-nowrap" style={{ paddingRight: "0.375em", fontSize: 18 }}>
{xConfig.username}
</div>
</div>
<div className="whitespace-nowrap text-secondary" style={{ fontSize: "1em", fontWeight: 400, lineHeight: "1.2" }} />
</div>
</div>) : <div className='flex w-full pb-3'></div>}
{
controls.showLogo ? <CardLogo xConfig={xConfig} /> : <></>
}
</div>
);
}
const CardLogo = ({ xConfig }) => {
return (<div className='flex justify-center'>
<img
id="x-logo"
src={XLogo.src}
alt=""
className=" h-[20px] w-[20px] right-4 top-4 opacity-30 dark:invert saturate-0 cursor-alias active:scale-90 transition-all ease-in-out"
/>
</div>
)
}
const Watermark = () => {
}
const CardBody: React.FC<{
xConfig: XConfig,
cardStyles: CardStore['cardStyles'],
}> = ({ xConfig, cardStyles }) => {
// cardStyles.imageLayout ||
const layout = xConfig.images.length >= 2 ? 'grid4' : 'vertical';
const images = _.compact(
[
...xConfig.images,
xConfig?.video?.poster,
...(xConfig?.links?.map((link) => link.src) || []),
]
)
return (
<div className="flex-grow flex flex-col justify-center">
<div className={cn("tweet-content text-lg leading-normal pointer-events-none mb-[1em] ")} style={{ fontSize: cardStyles.fontSize, overflowWrap: 'anywhere' }}>
<div className="content whitespace-pre-wrap" dir="auto">
{xConfig.text}
</div>
</div>
{images && images.length > 0 && (
<ImageLayout
images={images}
layout={layout}
/>
)}
</div>
)
}
const CardFooter = ({ xConfig }) => {
const time = _.isArray(xConfig) ? _.last(xConfig).time : xConfig.time;
return (
<div>
<div className="text-secondary-foreground flex items-center " style={{ paddingBottom: "0.5em", paddingTop: '0.5rem' }}>
<span>{formatTimestamp(time)}</span>
</div>
<div className='flex items-center justify-between'>
<div className="flex">
{['replies', 'shares', 'likes'].map((stat) => (
<div key={stat} className="whitespace-nowrap text-secondary-foreground" style={{ marginRight: "1em" }}>
<span className="font-medium" style={{ color: "var(--textPrimary)" }}>
{xConfig[stat]}
</span>{" "}
{stat}
</div>
))}
</div>
</div>
</div>
);
}
================================================
FILE: src/app/(app)/components/card-generator/wechat-card.tsx
================================================
import type { CardStore, XConfig } from "@src/hooks/useCardStore"
interface WechatCardProps {
xConfig: XConfig,
backgroundStyles: CardStore['backgroundStyles'],
cardStyles: CardStore['cardStyles'],
fontStyles: CardStore['fontStyles'],
}
export const WeChatCard: React.FC<WechatCardProps> = () => {
return (
<div
name="tempB"
className="sm:w-full w-[100vw] bg-[#1B1C1E] text-[#E3D2B3] flex flex-col justify-between tempB"
style={{
transition: "padding 500ms",
padding: 30,
fontFamily: "SourceHanSerifCN_SemiBold",
minHeight: "auto"
}}
>
<div>
<div
className="n-collapse-transition"
style={{ nBezier: "cubic-bezier(.4, 0, .2, 1)" }}
>
<div className="mb-2">
<div className="cursor-pointer flex w-max">
<input type="file" accept=".jpg, .jpeg, .png" className="hidden" />
<img
src="/_nuxt/default-avatar.C85jNu88.png"
id="icon"
className="rounded-[50%]"
style={{ width: 40, height: 40 }}
/>
</div>
</div>
</div>
<div
className="leading-[1.4] opacity-90 font-bold custom-transition-2 mb-1.5"
style={{ fontSize: "calc(1.25rem)" }}
>
<div className="bg-[transparent]">
<div contentEditable="true" translate="no" className="editable-element">
<p>Space</p>
</div>
</div>
</div>
<div
className="leading-[1.5] opacity-90 custom-transition-2 mb-6 mt-2"
style={{ fontSize: "calc(0.875rem)" }}
>
<div>
<div className="bg-[transparent]">
<div
contentEditable="true"
translate="no"
className="editable-element"
>
<p>摘录于2024/8/18</p>
</div>
</div>
</div>
</div>
<div
name="showContent"
className="content-body custom-transition-2 opacity-90"
style={{ fontSize: "calc(1.25rem)" }}
>
<div className="bg-[transparent]">
<div
contentEditable="true"
translate="no"
className="editable-element md-class"
>
<p>
人性和商业的能力,这样才能更好地发现市场痛点,了解用户需求,并用商业来完善产品想法,而不是仅仅作为一名员工去实现公司指定的产品路线。很多产品经理的工作多年间都无法得到进一步提升,其实相当一部分人是卡在了商业洞察这一块。于是很多人在问,是不是多读几本书就可以系统训练商业知识和商业嗅觉,从而增强自己的商业洞察能力?
</p>
</div>
</div>
</div>
</div>
<div>
<div className="mt-6" />
<div
className="color-[#BBBD9E] font-light opacity-70 leading-[1.4] custom-transition-2 mb-1.5"
style={{ fontSize: "calc(1rem)" }}
>
<div className="bg-[transparent]">
<div contentEditable="true" translate="no" className="editable-element">
<p>/产品之旅:产品经理的方法论与实战进阶</p>
</div>
</div>
</div>
<div
className="custom-transition-2 mb-1.5 opacity-70"
style={{ fontSize: "calc(0.9rem)" }}
>
<div className="bg-[transparent]">
<div contentEditable="true" translate="no" className="editable-element">
<p>赖京露</p>
</div>
</div>
</div>
<div>
<div className="divider bg-[#F6ECD4] opacity-10 h-px my-5" />
<div
className="qr-code-section flex items-center justify-between pt-2"
style={{ fontFamily: '"Noto Serif SC"' }}
>
<div>
<div
className="leading-[1.4] font-bold custom-transition-2"
style={{ fontSize: "calc(0.8rem)" }}
>
<div className="bg-[transparent]">
<div
contentEditable="true"
translate="no"
className="editable-element"
>
<p>微信读书</p>
</div>
</div>
</div>
</div>
<div className="bg-[#F6E2B5] p-1">
<div className="flex" id="qr-code">
<div
className="n-qr-code cursor-pointer"
style={{
padding: 0,
backgroundColor: "transparent",
width: 58,
height: 58,
borderRadius: 3
}}
>
<canvas
width={116}
height={116}
style={{ width: 58, height: 58 }}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
================================================
FILE: src/app/(app)/components/dynamic-style-tippy.tsx
================================================
import Tippy from "@tippyjs/react";
import { useEffect, useRef } from "react";
export const DynamicStyleTippyComponent = ({ children, content, tippyOptions = {} }) => {
const styleId = 'x-cards-dropdown-styles';
const tippyRef = useRef(null);
useEffect(() => {
// Check if the style element already exists
let styleElement = document.getElementById(styleId);
if (!styleElement) {
// If it doesn't exist, create it
styleElement = document.createElement('style');
styleElement.id = styleId;
document.head.appendChild(styleElement);
}
// Define your styles
const styles = `
.tippy-box[data-theme~='custom'] {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 240 4.9% 83.9%;
background-color: black;
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
border-radius: 6px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
overflow: hidden;
animation: scaleIn 0.2s ease-out;
}
.tippy-box[data-theme~='custom'] .tippy-content {
padding: 4px;
min-width: 152px;
}
.tippy-box[data-theme~='custom'] .dropdown-menu-list {
list-style-type: none;
padding: 0;
margin: 0;
}
.tippy-box[data-theme~='custom'] .dropdown-menu-item {
display: flex;
align-items: center;
padding: 8px 12px;
font-size: 14px;
color: hsl(var(--foreground));
cursor: pointer;
transition: background-color 0.2s, color 0.2s;
border-radius: 4px;
user-select: none;
}
.tippy-box[data-theme~='custom'] .dropdown-menu-item:hover {
background-color: hsl(var(--accent));
color: hsl(var(--accent-foreground));
}
.tippy-box[data-theme~='custom'] .dropdown-menu-item.no-hover:hover {
background-color: transparent;
color: hsl(var(--foreground));
}
.tippy-box[data-theme~='custom'] .dropdown-menu-item:active {
background-color: hsl(var(--muted));
}
.tippy-box[data-theme~='custom'] .dropdown-menu-icon {
width: 16px;
height: 16px;
margin-right: 8px;
color: var(--muted-foreground);
transition: color 0.2s;
}
.tippy-box[data-theme~='custom'] .dropdown-menu-icon:hover {
color: hsl(var(--accent-foreground));
}
.tippy-box[data-theme~='custom'] .dropdown-menu-separator {
display: flex;
align-items: center;
text-align: center;
// margin: 10px 0;
}
.tippy-box[data-theme~='custom'] .dropdown-menu-separator::before,
.tippy-box[data-theme~='custom'] .dropdown-menu-separator::after {
content: '';
flex: 1;
border-bottom: 1px solid hsl(var(--border));
}
.tippy-box[data-theme~='custom'] .dropdown-menu-separator::before {
margin-right: 0.5em;
}
.tippy-box[data-theme~='custom'] .dropdown-menu-separator::after {
margin-left: 0.5em;
}
.tippy-box[data-theme~='custom'] .dropdown-menu-separator-text {
padding: 0 10px;
font-size: 0.85em;
color: hsl(var(--muted-foreground));
white-space: nowrap;
}
.tippy-box[data-theme~='custom'] .dropdown-menu-item.danger {
color: hsl(var(--destructive-foreground));
}
.tippy-box[data-theme~='custom'] .dropdown-menu-item.danger:hover {
background-color: hsl(var(--destructive));
}
.tippy-box[data-theme~='custom'] .dropdown-menu-item.danger svg {
color: var(--destructive-foreground);
}
@keyframes scaleIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
`;
// Set the styles
styleElement.textContent = styles;
// Cleanup function
return () => {
};
}, []);
return (
<Tippy
ref={tippyRef}
content={content}
interactive={true}
appendTo={document.body}
placement="top-start"
theme="custom" // Use our custom theme
{...tippyOptions}
>
{children}
</Tippy>
);
};
================================================
FILE: src/app/(app)/components/hero.tsx
================================================
import chromeSvg from '@assets/chrome.svg';
import Image from "next/image";
const Hero = () => {
return (
<>
<div className='flex flex-col gap-y-8 justify-center'>
<div className="relative mx-auto flex max-w-2xl flex-col items-center">
<h2 className="text-center text-3xl font-medium text-gray-900 dark:text-gray-50 sm:text-6xl">
X Cards {' '}
<span className="animate-text-gradient inline-flex bg-gradient-to-r from-neutral-900 via-slate-500 to-neutral-500 bg-[200%_auto] bg-clip-text leading-tight text-transparent dark:from-neutral-100 dark:via-slate-400 dark:to-neutral-400">
Native Tweet Card service for X
</span>
</h2>
<p className="mt-6 text-center text-lg leading-6 text-gray-600 dark:text-gray-200">
Capture and share posts as beautiful images, cards, Markdown, or JSON, making sharing Twitter posts on other platforms more visual and attention-grabbing.
{' '}
</p>
<div className="mt-10 flex gap-4">
<a target="_blank" href="https://chromewebstore.google.com/detail/x-card/mbinooofmcjhjklihfejnkkebffceeop"
className="p-[3px] relative group">
<div className="absolute inset-0 bg-gradient-to-r from-[#8F01FF] to-[#FDB83B] rounded-lg" />
<div className="px-8 py-2 group-hover:scale-105 flex gap-x-3 rounded-[6px] relative group transition duration-200 text-white ">
<Image src={chromeSvg} alt="Chrome Extension" className="w-6 h-6" />
Download Extension
</div>
</a>
</div>
<a
className='mt-10'
href="https://www.producthunt.com/posts/x-cards?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-x-cards"
target="_blank"
>
<img
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=481821&theme=light"
alt="X Cards - Simple, Beautiful Tweets with Card Integration on X.com | Product Hunt"
style={{ width: 250, height: 54 }}
width={250}
height={54}
/>
</a>
</div>
{/* <VideoSection /> */}
</div>
</>
)
}
export default Hero
================================================
FILE: src/app/(app)/components/save-as-template-button.tsx
================================================
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@components/ui/alert-dialog";
import { Button } from "@components/ui/button";
import { Input } from "@components/ui/input";
import { useCardStore } from "@src/hooks/useCardStore";
import { useTemplatesStore } from "@src/hooks/useTemplatesStore";
import React from "react";
export const SaveAsTemplateButton = () => {
const cardStyles = useCardStore((state) => state.cardStyles);
const addTemplate = useTemplatesStore((state) => state.addTemplate);
const backgroundStyles = useCardStore((state) => state.backgroundStyles);
const [name, setName] = React.useState('');
return (<AlertDialog>
<AlertDialogTrigger asChild>
<Button size="sm" >Save as Template</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Input Template Name</AlertDialogTitle>
{/* <AlertDialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</AlertDialogDescription> */}
<Input value={name} onChange={(e) => {
setName(e.target.value)
}}></Input>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => addTemplate({
name: name,
colorIndex: useCardStore.getState().colorIndex,
backgroundStyles: backgroundStyles,
tabConfig: useCardStore.getState().tabConfig,
cardStyles: cardStyles
})}>Confirm</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>)
}
================================================
FILE: src/app/(app)/components/sections/FeaturesGridSection.tsx
================================================
// import yellowWaveDown from "@/public/images/banner-wave.png";
// import Image from "next/image";
import { BookCopy, LayoutGrid, ListCollapse, Palette } from "lucide-react";
import { useId } from "react";
function FeaturesGridSection() {
return (
<>
<div className="relative py-32">
<div className="flex flex-col items-center justify-center ">
<h1 className="m-[33.2px_0px_8px] h-[46px] max-w-screen-xl text-center font-semibold text-[40px] leading-[46px]">
Features
</h1>
<div className="max-w-screen-xl p-[0px_25px] text-center text-xl tracking-[-0.2px]">
<p>
We provide a variety of features to help you create beautiful
images, cards, Markdown, or JSON from tweets, making sharing
Twitter posts on other platforms more visual and
attention-grabbing.
</p>
</div>
</div>
<div className="m-auto mx-auto mt-[50px] flex max-w-7xl flex-row flex-wrap justify-center overflow-x-hidden ">
{grid.map((feature) => (
<div
key={feature.title}
className="relative m-4 w-[260px] min-w-[260px] overflow-hidden rounded-3xl bg-gradient-to-b from-neutral-100 to-white p-6 dark:from-neutral-900 dark:to-neutral-950"
>
<Grid size={20} />
<div className="mb-2 p-0">{feature?.icon}</div>
<p className="relative z-20 font-bold text-base text-neutral-800 dark:text-white">
{feature.title}
</p>
<p className="relative z-20 mt-4 font-normal text-base text-neutral-600 dark:text-neutral-400">
{feature.description}
</p>
</div>
))}
</div>
</div>
{/* <Image src={yellowWaveDown} alt="" className=" -mt-32 absolute" /> */}
</>
);
}
const grid = [
{
icon: (
<BookCopy className="w-8 h-8" />
),
title: "Fast capture",
// description:
// "快速将任何帖子转换为卡片,只需要亲亲点击"
description:
"Capture and share posts as beautiful images, cards, Markdown, or JSON, making sharing Twitter posts on other platforms more visual and attention-grabbing."
},
{
icon: (
<Palette className="w-8 h-8" />
),
title: "Custom Styles",
// description:
// "多种卡片风格,自定义颜色,边距等等"
description:
"Multiple card styles, custom colors, margins, and more."
},
{
icon: (
<ListCollapse className="w-8 h-8" />
),
title: "Combining multiple posts",
// description:
// "拼接多个tweet内容,生成长卡片",
description:
"Combine multiple tweet contents to generate long cards."
},
{
icon: (
<LayoutGrid className="w-8 h-8" />
),
title: "Dynamic management",
// description:
// "动态管理你的帖子,随时删除,添加",
description:
"Manage your posts dynamically, delete or add them at any time."
},
];
const Grid = ({
pattern,
size,
}: {
pattern?: number[][];
size?: number;
}) => {
const p = pattern ?? [
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
[Math.floor(Math.random() * 4) + 7, Math.floor(Math.random() * 6) + 1],
];
return (
<div className="-ml-20 -mt-2 pointer-events-none absolute top-0 left-1/2 h-full [mask-image:linear-gradient(white,transparent)]">
<div className="absolute inset-0 bg-gradient-to-r from-zinc-100/30 to-zinc-300/30 opacity-100 [mask-image:radial-gradient(farthest-side_at_top,white,transparent)] dark:from-zinc-900/30 dark:to-zinc-900/30">
<GridPattern
width={size ?? 20}
height={size ?? 20}
x="-12"
y="4"
squares={p}
className="absolute inset-0 h-full w-full fill-black/10 stroke-black/10 mix-blend-overlay dark:fill-white/10 dark:stroke-white/10"
/>
</div>
</div>
);
};
function GridPattern({ width, height, x, y, squares, ...props }: any) {
const patternId = useId();
return (
<svg aria-hidden="true" {...props}>
<defs>
<pattern
id={patternId}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path d={`M.5 ${height}V.5H${width}`} fill="none" />
</pattern>
</defs>
<rect
width="100%"
height="100%"
strokeWidth={0}
fill={`url(#${patternId})`}
/>
{squares && (
<svg x={x} y={y} className="overflow-visible">
<title>noise</title>
{squares.map(([x, y]: any) => (
<rect
strokeWidth="0"
key={`${x}-${y}`}
width={width + 1}
height={height + 1}
x={x * width}
y={y * height}
/>
))}
</svg>
)}
</svg>
);
}
export default FeaturesGridSection;
================================================
FILE: src/app/(app)/components/sections/features2.tsx
================================================
// import { Icon } from "@/components/ui/icon";
import { Card, CardContent, CardHeader, CardTitle } from "@src/components/ui/card";
import { Icon } from "@src/components/ui/icon";
import { icons } from "lucide-react";
interface FeaturesProps {
icon: string;
title: string;
description: string;
}
const featureList: FeaturesProps[] = [
{
icon: "Globe",
title: "Seamless Integration",
description: "Integrate directly on x.com without any hassle. Our product works smoothly within the platform you're already using.",
},
{
icon: "MousePointerClick",
title: "One-Click Generation",
description: "Simple and easy to use. Generate and copy cards with just a single click, streamlining your workflow.",
},
{
icon: "Palette",
title: "Customizable Design",
description: "Personalize your cards by modifying styles, background colors, and more to match your brand or preferences.",
},
{
icon: "Eye",
title: "Real-Time Preview",
description: "See your changes instantly and manage your tweets dynamically, ensuring your content looks perfect before posting.",
},
{
icon: "FileText",
title: "Long-Form Support",
description: "Create and manage longer posts effortlessly, perfect for in-depth content or thread creation.",
},
{
icon: "Lock",
title: "Open Source & Secure",
description: "Our product is open source, ensuring transparency and allowing for community contributions while maintaining high security standards.",
},
];
export const FeaturesSection = () => {
return (
<section id="features" className="container py-24 sm:py-32">
<h2 className="text-lg text-primary text-center mb-2 tracking-wider">
Features
</h2>
<h2 className="text-3xl md:text-4xl text-center font-bold mb-4">
Why Choose Our Twitter Card Generator
</h2>
<h3 className="md:w-1/2 mx-auto text-xl text-center text-muted-foreground mb-8">
Elevate your Twitter presence with our seamless, customizable, and secure card generator.
Create eye-catching content directly on x.com with just a few clicks.
</h3>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{featureList.map(({ icon, title, description }) => (
<div key={title}>
<Card className="h-full bg-background border-0 shadow-none">
<CardHeader className="flex justify-center items-center">
<div className="bg-primary/20 p-2 rounded-full ring-8 ring-primary/10 mb-4">
<Icon
name={icon as keyof typeof icons}
size={24}
color="hsl(var(--primary))"
className="text-primary"
/>
</div>
<CardTitle>{title}</CardTitle>
</CardHeader>
<CardContent className="text-muted-foreground text-center">
{description}
</CardContent>
</Card>
</div>
))}
</div>
</section>
);
};
================================================
FILE: src/app/(app)/components/sections/footer.tsx
================================================
import Link from "next/link";
import Logo from "@assets/icon.png";
export const FooterSection = () => {
return (
<footer id="footer" className="container py-24 sm:py-32">
<div className="p-10 bg-card border border-secondary rounded-2xl">
<div className="grid grid-cols-2 md:grid-cols-4 xl:grid-cols-6 gap-x-12 gap-y-8">
<div className="col-span-full xl:col-span-2">
<Link href="#" className="flex font-bold items-center">
{/* <ChevronsDownIcon className="w-9 h-9 mr-2 bg-gradient-to-tr from-primary via-primary/70 to-primary rounded-lg border border-secondary" /> */}
<img className="w-9 h-9 mr-2 " src={Logo.src} alt="xcards" ></img>
<h3 className="text-2xl">X Cards</h3>
</Link>
</div>
<div className="flex flex-col gap-2">
<h3 className="font-bold text-lg">Contact</h3>
<div>
<Link href="https://github.com/hzeyuan/x-cards" className="opacity-60 hover:opacity-100">
Github
</Link>
</div>
<div>
<Link href="https://x.com/FeigelC35583" className="opacity-60 hover:opacity-100">
Twitter
</Link>
</div>
</div>
<div className="flex flex-col gap-2">
<h3 className="font-bold text-lg">Socials</h3>
<div>
<Link href="https://discord.gg/Prjas7Qh" className="opacity-60 hover:opacity-100">
Discord
</Link>
</div>
</div>
</div>
{/* <Separator className="my-6" /> */}
<section className="">
<h3 className="">
© 2024 Designed and developed by
<Link
target="_blank"
href="https://x.com/FeigelC35583"
className="text-primary transition-all border-primary hover:border-b-2 ml-1"
>
Zane Ryan
</Link>
</h3>
</section>
</div>
</footer>
);
};
================================================
FILE: src/app/(app)/components/sections/video.tsx
================================================
"use client"
import ReactPlayer from 'react-player'
const VideoSection = () => {
return (<div className='mx-auto'>
<ReactPlayer url="https://youtu.be/okCIZrFrTCE" />
</div>)
}
export default VideoSection
================================================
FILE: src/app/(app)/components/template-list.tsx
================================================
import { Button } from "@components/ui/button";
import { useCardStore } from "@src/hooks/useCardStore";
import { useTemplatesStore } from "@src/hooks/useTemplatesStore";
import { X } from "lucide-react";
export const TemplateList = () => {
const templates = useTemplatesStore(state => state.templates);
const delTemplate = useTemplatesStore(state => state.delTemplate);
const setFontStyles = useCardStore(state => state.setFontStyles);
const setColorIndex = useCardStore(state => state.setColorIndex);
const updateBackgroundStyles = useCardStore(state => state.updateBackgroundStyles);
const updateCardStyles = useCardStore(state => state.updateCardStyles);
return (
<div className="flex gap-4 py-4">
{templates.map((t, index) => {
return (
<Button size="sm" key={index} variant="secondary"
onClick={() => {
setColorIndex(t.colorIndex)
updateBackgroundStyles(t.backgroundStyles)
updateCardStyles(t.cardStyles)
}
}
className="hover:shadow-md">
<span>
<div>
<div className="action-btn flex-shrink-0 flex items-center justify-center cursor-pointer rounded-[4px] text-accent-foreground ">
{t.name}
</div>
</div>
</span>
<X className=" cursor-pointer hover:shadw-md ml-1 w-3 h-3" onClick={(e) => {
e.preventDefault()
e.stopPropagation()
delTemplate(index)
}} />
</Button>
)
})}
</div>
)
}
================================================
FILE: src/app/(app)/components/x-form.tsx
================================================
import { Button } from "@components/ui/button";
import { Input } from "@components/ui/input";
import { useCardStore, type XConfig } from "@src/hooks/useCardStore";
import { useState } from "react";
interface XFormProps {
}
export const XForm = (props: XFormProps) => {
const [url, setUrl] = useState<string>('');
const setXconfig = useCardStore(state => state.setXConfig);
const handleGetX = async (url: string) => {
const res = await fetch(`/api/x?url=${url}`, {
method: 'GET',
})
const data = await res.json();
console.log('res', res);
setXconfig({
...data.data,
images: [data.data.imageUrl]
} as XConfig)
}
return (
<div className="flex w-full pt-8 items-center space-x-2">
<Input onChange={(e) => {
setUrl(e.target.value);
}} value={url} type="url" placeholder="input X url" />
<Button type="submit" onClick={() => {
handleGetX(url);
}}>Get Tweet →</Button>
</div>
)
}
================================================
FILE: src/app/(app)/layout.tsx
================================================
// "use client"
import Image from 'next/image';
import Logo from '@assets/icon.png';
const AppLayout = ({ children }) => {
return (
<div>
<header
style={{
zIndex: 9999
}}
className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="bg-gradient-to-br from-green-300 via-blue-500 to-purple-600 fixed w-full px-4 py-2 text-white !bg-gradient-to-r">
<p className="flex items-center justify-center text-sm font-medium !text-white">
{" "}
X cards also support the chrome extension version.{" "}
<a
className="underline flex items-center"
href="https://chromewebstore.google.com/detail/x-card/mbinooofmcjhjklihfejnkkebffceeop"
target="_blank"
>
<span className="ml-2 text-xs">Get Chrome Extension →</span>
</a>
</p>
</div>
<div className=" relative top-[57px] container flex h-14 max-w-screen-2xl items-center">
<div className="mr-4 hidden md:flex">
<a className="mr-6 flex items-center space-x-2" href="/">
<Image className='w-6 h-6' alt="logo" src={Logo} width={24} ></Image>
<span className="hidden sm:inline-block whitespace-nowrap">X Cards</span>
</a>
</div>
<div className="w-full flex justify-center">
<nav className="flex items-center gap-4 text-sm lg:gap-6">
</nav>
</div>
<button
className="inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:text-accent-foreground h-9 py-2 mr-2 px-0 text-base hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 focus-visible:ring-offset-0 md:hidden"
type="button"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="radix-:R4mja:"
data-state="closed"
>
<svg
strokeWidth="1.5"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
>
<path
d="M3 5H11"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3 12H16"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3 19H21"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<
gitextract_nbnj719d/ ├── .github/ │ └── workflows/ │ └── submit.yml ├── .gitignore ├── .prettierrc.mjs ├── LICENSE ├── README.md ├── README_ZH.md ├── components/ │ └── ui/ │ ├── EditableButton.tsx │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── color-picker.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── popover.tsx │ ├── radio-group.tsx │ ├── select-position.tsx │ ├── select.tsx │ ├── slider.tsx │ ├── sonner.tsx │ ├── switch.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ └── use-toast.ts ├── components.json ├── lib/ │ └── utils.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── popup.tsx ├── postcss.config.js ├── src/ │ ├── app/ │ │ ├── (app)/ │ │ │ ├── components/ │ │ │ │ ├── FrequentlyAskedQuestions.tsx │ │ │ │ ├── GoogleFontSelector.tsx │ │ │ │ ├── ImageLayout.tsx │ │ │ │ ├── LazyLoadAnimatedSection.tsx │ │ │ │ ├── ResultIcon.tsx │ │ │ │ ├── card-generator/ │ │ │ │ │ ├── color.tsx │ │ │ │ │ ├── controller/ │ │ │ │ │ │ ├── background-controller.tsx │ │ │ │ │ │ ├── card-controller.tsx │ │ │ │ │ │ ├── font-controller.tsx │ │ │ │ │ │ ├── iframe-controller.tsx │ │ │ │ │ │ └── input-controller.tsx │ │ │ │ │ ├── display.tsx │ │ │ │ │ ├── export-tab.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── twitter-card.tsx │ │ │ │ │ └── wechat-card.tsx │ │ │ │ ├── dynamic-style-tippy.tsx │ │ │ │ ├── hero.tsx │ │ │ │ ├── save-as-template-button.tsx │ │ │ │ ├── sections/ │ │ │ │ │ ├── FeaturesGridSection.tsx │ │ │ │ │ ├── features2.tsx │ │ │ │ │ ├── footer.tsx │ │ │ │ │ └── video.tsx │ │ │ │ ├── template-list.tsx │ │ │ │ └── x-form.tsx │ │ │ ├── layout.tsx │ │ │ ├── page.tsx │ │ │ └── request.tsx │ │ ├── (extension)/ │ │ │ ├── independent/ │ │ │ │ ├── components/ │ │ │ │ │ └── index.tsx │ │ │ │ └── page.tsx │ │ │ └── welcome/ │ │ │ └── page.tsx │ │ ├── api/ │ │ │ ├── license/ │ │ │ │ └── route.ts │ │ │ └── x/ │ │ │ └── route.ts │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── utils/ │ │ ├── IFrameMessageSystem.ts │ │ ├── element.ts │ │ ├── export.ts │ │ ├── format.ts │ │ ├── image.ts │ │ └── index.ts │ ├── background/ │ │ ├── index.ts │ │ └── messages/ │ │ ├── code.ts │ │ └── tweet.ts │ ├── components/ │ │ ├── extension/ │ │ │ ├── card-button.tsx │ │ │ ├── input-code.tsx │ │ │ ├── label-with-icon.tsx │ │ │ ├── layout-options.tsx │ │ │ ├── preset-color-list.tsx │ │ │ ├── tabs.tsx │ │ │ ├── tweet-manager.tsx │ │ │ ├── use-tweet-collection.ts │ │ │ └── x-cards-toast/ │ │ │ ├── font-control.tsx │ │ │ ├── image-preview.tsx │ │ │ ├── index.module.css │ │ │ ├── index.tsx │ │ │ ├── padding-control.tsx │ │ │ ├── scale-control.tsx │ │ │ └── tweet-control.tsx │ │ ├── sortableList.tsx │ │ └── ui/ │ │ ├── BentoGrid.tsx │ │ ├── DotPattern.tsx │ │ ├── acetenity-tabs.tsx │ │ ├── animated-list.tsx │ │ ├── animatedBeam.tsx │ │ ├── api-key-panel.tsx │ │ ├── bold-copy.tsx │ │ ├── burnIn.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── fade-text.tsx │ │ ├── grid-pattern.tsx │ │ ├── grid.tsx │ │ ├── icon.tsx │ │ ├── loading-spinner.tsx │ │ ├── marquee.tsx │ │ ├── scroll-based-velocity.tsx │ │ ├── shimmer-button.tsx │ │ ├── text-generate-effect.tsx │ │ └── underline-hover-text.tsx │ ├── config/ │ │ └── site.ts │ ├── contents/ │ │ ├── plasmo-overlay.css │ │ ├── plasmo-overlay.tsx │ │ ├── x-home.tsx │ │ └── x.css │ ├── hooks/ │ │ ├── useCardStore.tsx │ │ └── useTemplatesStore.tsx │ ├── lib/ │ │ └── BlurGradientBg.module.js │ └── sandbox.tsx ├── tailwind.config.ts ├── tsconfig.json └── vercel.json
SYMBOL INDEX (164 symbols across 50 files)
FILE: components/ui/badge.tsx
type BadgeProps (line 26) | interface BadgeProps
function Badge (line 30) | function Badge({ className, variant, ...props }: BadgeProps) {
FILE: components/ui/button.tsx
type ButtonProps (line 36) | interface ButtonProps
FILE: components/ui/color-picker.tsx
function useForwardedRef (line 14) | function useForwardedRef<T>(ref: React.ForwardedRef<T>) {
type ColorPickerProps (line 30) | interface ColorPickerProps {
FILE: components/ui/input.tsx
type InputProps (line 5) | interface InputProps
FILE: components/ui/sonner.tsx
type ToasterProps (line 6) | type ToasterProps = React.ComponentProps<typeof Sonner>
FILE: components/ui/textarea.tsx
type TextareaProps (line 5) | interface TextareaProps
FILE: components/ui/toast.tsx
type ToastProps (line 115) | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement (line 117) | type ToastActionElement = React.ReactElement<typeof ToastAction>
FILE: components/ui/toaster.tsx
function Toaster (line 13) | function Toaster() {
FILE: components/ui/use-toast.ts
constant TOAST_LIMIT (line 11) | const TOAST_LIMIT = 1
constant TOAST_REMOVE_DELAY (line 12) | const TOAST_REMOVE_DELAY = 1000000
type ToasterToast (line 14) | type ToasterToast = ToastProps & {
function genId (line 30) | function genId() {
type ActionType (line 35) | type ActionType = typeof actionTypes
type Action (line 37) | type Action =
type State (line 55) | interface State {
function dispatch (line 136) | function dispatch(action: Action) {
type Toast (line 143) | type Toast = Omit<ToasterToast, "id">
function toast (line 145) | function toast({ ...props }: Toast) {
function useToast (line 174) | function useToast() {
FILE: lib/utils.ts
function cn (line 4) | function cn(...inputs: ClassValue[]) {
FILE: src/app/(app)/components/GoogleFontSelector.tsx
function GoogleFontSelector (line 36) | function GoogleFontSelector({ onFontChange }) {
FILE: src/app/(app)/components/ResultIcon.tsx
type PropTypes (line 7) | type PropTypes = {
FILE: src/app/(app)/components/card-generator/color.tsx
type ColorInputPropTypes (line 157) | type ColorInputPropTypes = {
FILE: src/app/(app)/components/card-generator/controller/font-controller.tsx
type FontControllerProps (line 6) | interface FontControllerProps {
FILE: src/app/(app)/components/card-generator/twitter-card.tsx
type TwitterCardProps (line 9) | interface TwitterCardProps {
FILE: src/app/(app)/components/card-generator/wechat-card.tsx
type WechatCardProps (line 3) | interface WechatCardProps {
FILE: src/app/(app)/components/sections/FeaturesGridSection.tsx
function FeaturesGridSection (line 6) | function FeaturesGridSection() {
function GridPattern (line 126) | function GridPattern({ width, height, x, y, squares, ...props }: any) {
FILE: src/app/(app)/components/sections/features2.tsx
type FeaturesProps (line 5) | interface FeaturesProps {
FILE: src/app/(app)/components/x-form.tsx
type XFormProps (line 6) | interface XFormProps {
FILE: src/app/(extension)/independent/components/index.tsx
function generateStaticParams (line 11) | async function generateStaticParams() {
FILE: src/app/(extension)/independent/page.tsx
function generateStaticParams (line 3) | async function generateStaticParams() {
FILE: src/app/layout.tsx
function RootLayout (line 22) | function RootLayout({
FILE: src/app/utils/IFrameMessageSystem.ts
type MessageCallback (line 1) | type MessageCallback = (value: any) => void;
type MessageEvent (line 3) | interface MessageEvent {
class IFrameMessageSystem (line 10) | class IFrameMessageSystem {
method constructor (line 13) | constructor() {
method subscribe (line 19) | subscribe(action: string, callback: MessageCallback): () => void {
method unsubscribe (line 27) | unsubscribe(action: string, callback: MessageCallback): void {
method publish (line 33) | publish(iframe: HTMLIFrameElement, action: string, data: any, timeout:...
method handleMessage (line 56) | private handleMessage(event: MessageEvent): void {
FILE: src/app/utils/element.ts
function traverseAndCheck (line 1) | function traverseAndCheck(element, condition) {
function getAdjacentCellDiv (line 19) | function getAdjacentCellDiv(element, direction) {
function checkAppliedStyle (line 30) | function checkAppliedStyle(element, properties) {
FILE: src/app/utils/format.ts
function formatTimestamp (line 1) | function formatTimestamp(timestamp: number): string {
FILE: src/app/utils/index.ts
function checkTheVerticalLine (line 10) | function checkTheVerticalLine(element) {
function isElementVisible (line 20) | function isElementVisible(element) {
type CopyImage (line 91) | type CopyImage = (tweetInfo: XConfig | XConfig[], cardConfig: any) => Pr...
function extractTweetInfo (line 159) | function extractTweetInfo(postElement) {
FILE: src/components/extension/label-with-icon.tsx
type LabelWithIconProps (line 4) | interface LabelWithIconProps {
FILE: src/components/extension/use-tweet-collection.ts
type TweetControlState (line 5) | interface TweetControlState {
type CardConfig (line 13) | interface CardConfig {
type TweetCollection (line 24) | interface TweetCollection {
FILE: src/components/extension/x-cards-toast/index.tsx
type PreviewToastProps (line 24) | interface PreviewToastProps {
FILE: src/components/extension/x-cards-toast/scale-control.tsx
type ScaleControlProps (line 6) | interface ScaleControlProps {
FILE: src/components/extension/x-cards-toast/tweet-control.tsx
type TweetControlProps (line 8) | interface TweetControlProps {
type ControlOptionProps (line 66) | interface ControlOptionProps {
FILE: src/components/sortableList.tsx
type Item (line 17) | type Item<T> = T & {
type SortableListItemProps (line 21) | interface SortableListItemProps<T> {
function SortableListItem (line 33) | function SortableListItem<T>({
type SortableListProps (line 130) | interface SortableListProps<T> {
function SortableList (line 142) | function SortableList<T>({
FILE: src/components/ui/DotPattern.tsx
type DotPatternProps (line 5) | interface DotPatternProps {
function DotPattern (line 16) | function DotPattern({
FILE: src/components/ui/acetenity-tabs.tsx
type Tab (line 7) | type Tab = {
FILE: src/components/ui/animated-list.tsx
type AnimatedListProps (line 6) | interface AnimatedListProps {
function AnimatedListItem (line 46) | function AnimatedListItem({ children }: { children: React.ReactNode }) {
FILE: src/components/ui/animatedBeam.tsx
type AnimatedBeamProps (line 8) | interface AnimatedBeamProps {
FILE: src/components/ui/bold-copy.tsx
function BoldCopy (line 9) | function BoldCopy({
FILE: src/components/ui/burnIn.tsx
type BlurIntProps (line 7) | interface BlurIntProps {
FILE: src/components/ui/chart.tsx
constant THEMES (line 14) | const THEMES = { light: "", dark: ".dark" } as const
type ChartConfig (line 16) | type ChartConfig = {
type ChartContextProps (line 26) | type ChartContextProps = {
function useChart (line 32) | function useChart() {
function getPayloadConfigFromPayload (line 325) | function getPayloadConfigFromPayload(
FILE: src/components/ui/fade-text.tsx
type FadeTextProps (line 6) | type FadeTextProps = {
function FadeText (line 13) | function FadeText({
FILE: src/components/ui/grid-pattern.tsx
type GridPatternProps (line 5) | interface GridPatternProps {
function GridPattern (line 16) | function GridPattern({
FILE: src/components/ui/grid.tsx
type GridProps (line 1) | interface GridProps {
function Placeholder (line 18) | function Placeholder({ size = 20 }: Pick<GridProps, "size">) {
function Grid (line 35) | function Grid({ color = "#cacaca", size = 20, children }: GridProps) {
FILE: src/components/ui/marquee.tsx
type MarqueeProps (line 3) | interface MarqueeProps {
function Marquee (line 13) | function Marquee({
FILE: src/components/ui/scroll-based-velocity.tsx
type VelocityScrollProps (line 16) | interface VelocityScrollProps {
type ParallaxProps (line 22) | interface ParallaxProps {
function VelocityScroll (line 33) | function VelocityScroll({
FILE: src/components/ui/shimmer-button.tsx
type ShimmerButtonProps (line 5) | interface ShimmerButtonProps
FILE: src/components/ui/underline-hover-text.tsx
type UnderlineHoverTextProps (line 6) | interface UnderlineHoverTextProps {
FILE: src/hooks/useCardStore.tsx
type Frame (line 5) | type Frame = 'none' | 'macos' | 'windows'
type SocialPlatform (line 7) | type SocialPlatform = 'instagram' | 'facebook' | 'linkedin' | 'whatsapp'...
type PostType (line 9) | type PostType = 'landscape' | 'square' | 'portrait' | 'post' | 'story' |...
type AspectRatio (line 11) | type AspectRatio = '16:9' | '3:2' | '4:3' | '5:4' | '1:1' | '4:5' | '3:4...
type ImageDimensions (line 14) | interface ImageDimensions {
type ImageLayoutProps (line 21) | type ImageLayoutProps = {
type LayoutOption (line 28) | interface LayoutOption {
type XConfig (line 84) | interface XConfig {
type CardStore (line 108) | interface CardStore {
FILE: src/hooks/useTemplatesStore.tsx
type Template (line 8) | type Template = Pick<CardStore, 'colorIndex' | 'backgroundStyles' | 'car...
type TemplatesStore (line 25) | interface TemplatesStore {
FILE: src/lib/BlurGradientBg.module.js
function t (line 1) | function t(t,e){(null==e||e>t.length)&&(e=t.length);for(var i=0,r=Array(...
function e (line 1) | function e(t,e,i){return e=a(e),u(t,o()?Reflect.construct(e,i||[],a(t).c...
function i (line 1) | function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a ...
function r (line 1) | function r(t,e){for(var i=0;i<e.length;i++){var r=e[i];r.enumerable=r.en...
function n (line 1) | function n(t,e,i){return e&&r(t.prototype,e),i&&r(t,i),Object.defineProp...
function s (line 1) | function s(){return s="undefined"!=typeof Reflect&&Reflect.get?Reflect.g...
function a (line 1) | function a(t){return a=Object.setPrototypeOf?Object.getPrototypeOf.bind(...
function h (line 1) | function h(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("S...
function o (line 1) | function o(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct...
function u (line 1) | function u(t,e){if(e&&("object"==typeof e||"function"==typeof e))return ...
function l (line 1) | function l(t,e){return l=Object.setPrototypeOf?Object.setPrototypeOf.bin...
function c (line 1) | function c(e){return function(e){if(Array.isArray(e))return t(e)}(e)||fu...
function f (line 1) | function f(t){var e=function(t,e){if("object"!=typeof t||!t)return t;var...
function d (line 1) | function d(t){return d="function"==typeof Symbol&&"symbol"==typeof Symbo...
function g (line 1) | function g(e,i){if(e){if("string"==typeof e)return t(e,i);var r={}.toStr...
function v (line 1) | function v(t){var e="function"==typeof Map?new Map:void 0;return v=funct...
function p (line 1) | function p(t){var e=t[0],i=t[1],r=t[2];return Math.sqrt(e*e+i*i+r*r)}
function m (line 1) | function m(t,e){return t[0]=e[0],t[1]=e[1],t[2]=e[2],t}
function y (line 1) | function y(t,e,i){return t[0]=e[0]+i[0],t[1]=e[1]+i[1],t[2]=e[2]+i[2],t}
function _ (line 1) | function _(t,e,i){return t[0]=e[0]-i[0],t[1]=e[1]-i[1],t[2]=e[2]-i[2],t}
function b (line 1) | function b(t,e,i){return t[0]=e[0]*i,t[1]=e[1]*i,t[2]=e[2]*i,t}
function x (line 1) | function x(t){var e=t[0],i=t[1],r=t[2];return e*e+i*i+r*r}
function E (line 1) | function E(t,e){var i=e[0],r=e[1],n=e[2],s=i*i+r*r+n*n;return s>0&&(s=1/...
function w (line 1) | function w(t,e){return t[0]*e[0]+t[1]*e[1]+t[2]*e[2]}
function M (line 1) | function M(t,e,i){var r=e[0],n=e[1],s=e[2],a=i[0],h=i[1],o=i[2];return t...
function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
function U (line 1) | function U(t,e,i,r){r=r.length?function(t){var e=t.length,i=t[0].length;...
function L (line 1) | function L(t){I>100||I++}
function G (line 1) | function G(t,e,i){var r=e[0],n=e[1],s=e[2],a=e[3],h=i[0],o=i[1],u=i[2],l...
function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
function Z (line 1) | function Z(t){var e=t[0],i=t[1],r=t[2],n=t[3],s=t[4],a=t[5],h=t[6],o=t[7...
function Q (line 1) | function Q(t,e,i){var r=e[0],n=e[1],s=e[2],a=e[3],h=e[4],o=e[5],u=e[6],l...
function K (line 1) | function K(t,e){var i=e[0],r=e[1],n=e[2],s=e[4],a=e[5],h=e[6],o=e[8],u=e...
function tt (line 1) | function tt(t,e,i){return t[0]=e[0]+i[0],t[1]=e[1]+i[1],t[2]=e[2]+i[2],t...
function et (line 1) | function et(t,e,i){return t[0]=e[0]-i[0],t[1]=e[1]-i[1],t[2]=e[2]-i[2],t...
function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
function r (line 1) | function r(t){var n,s=arguments.length>1&&void 0!==arguments[1]?argument...
function lt (line 1) | function lt(t,e,i){var r=e[0],n=e[1],s=e[2],a=e[3],h=e[4],o=e[5],u=e[6],...
function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
function r (line 1) | function r(t){var n,s=arguments.length>1&&void 0!==arguments[1]?argument...
function vt (line 1) | function vt(t){return!(t&t-1)}
function bt (line 1) | function bt(t){4===t.length&&(t=t[0]+t[1]+t[1]+t[2]+t[2]+t[3]+t[3]);var ...
function xt (line 1) | function xt(t){return[((t=parseInt(t))>>16&255)/255,(t>>8&255)/255,(255&...
function Et (line 1) | function Et(t){return void 0===t?[0,0,0]:3===arguments.length?arguments:...
function r (line 1) | function r(t){var n;return i(this,r),Array.isArray(t)?u(n,n=e(this,r,c(t...
function kt (line 1) | function kt(t,e,i){return t[0]=e[0]+i[0],t[1]=e[1]+i[1],t}
function At (line 1) | function At(t,e,i){return t[0]=e[0]-i[0],t[1]=e[1]-i[1],t}
function Tt (line 1) | function Tt(t,e,i){return t[0]=e[0]*i,t[1]=e[1]*i,t}
function Rt (line 1) | function Rt(t){var e=t[0],i=t[1];return Math.sqrt(e*e+i*i)}
function Ft (line 1) | function Ft(t,e){return t[0]*e[1]-t[1]*e[0]}
function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
function r (line 1) | function r(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[...
function l (line 1) | function l(u,l,d){var m=[],y=v(g((l=1==l?{entropy:!0}:l||{}).entropy?[u,...
function c (line 1) | function c(t){var e,i=t.length,r=this,s=0,a=r.i=r.j=0,h=r.S=[];for(i||(t...
function f (line 1) | function f(t,e){return e.i=t.i,e.j=t.j,e.S=t.S.slice(),e}
function g (line 1) | function g(t,e){var i,r=[],n=d(t);if(e&&"object"==n)for(i in t)try{r.pus...
function v (line 1) | function v(t,e){for(var i,r=t+"",n=0;n<r.length;)e[u&n]=u&(i^=19*e[u&n])...
function p (line 1) | function p(t){return String.fromCharCode.apply(0,t)}
function r (line 1) | function r(){var t,n=arguments.length>0&&void 0!==arguments[0]?arguments...
FILE: tailwind.config.ts
function addVariablesForColors (line 158) | function addVariablesForColors({ addBase, theme }: any) {
Condensed preview — 125 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (515K chars).
[
{
"path": ".github/workflows/submit.yml",
"chars": 932,
"preview": "name: \"Submit to Web Store\"\non:\n workflow_dispatch:\n\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses:"
},
{
"path": ".gitignore",
"chars": 349,
"preview": "\n# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.p"
},
{
"path": ".prettierrc.mjs",
"chars": 558,
"preview": "/**\n * @type {import('prettier').Options}\n */\nexport default {\n printWidth: 80,\n tabWidth: 2,\n useTabs: false,\n semi"
},
{
"path": "LICENSE",
"chars": 16724,
"preview": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\""
},
{
"path": "README.md",
"chars": 5724,
"preview": "<a name=\"readme-top\"></a>\n\n<div align=\"center\">\n<img src=\"assets/icon.png\" width=\"32\" >\n<h1>X Cards</h1>\n\n[English](READ"
},
{
"path": "README_ZH.md",
"chars": 3856,
"preview": "<a name=\"readme-top\"></a>\n\n<div align=\"center\">\n<img src=\"assets/icon.png\" width=\"32\" >\n<h1>X Cards</h1>\n\n[English](READ"
},
{
"path": "components/ui/EditableButton.tsx",
"chars": 1387,
"preview": "import React, { useState, useRef } from 'react';\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from "
},
{
"path": "components/ui/accordion.tsx",
"chars": 2118,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { Ch"
},
{
"path": "components/ui/alert-dialog.tsx",
"chars": 4434,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimpor"
},
{
"path": "components/ui/badge.tsx",
"chars": 1128,
"preview": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/"
},
{
"path": "components/ui/button.tsx",
"chars": 1835,
"preview": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class"
},
{
"path": "components/ui/color-picker.tsx",
"chars": 2625,
"preview": "'use client';\n\nimport { forwardRef, useEffect, useMemo, useRef, useState } from 'react';\nimport { HexColorPicker } from "
},
{
"path": "components/ui/dropdown-menu.tsx",
"chars": 7309,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimpo"
},
{
"path": "components/ui/input.tsx",
"chars": 824,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface InputProps\n extends React.InputHTMLA"
},
{
"path": "components/ui/popover.tsx",
"chars": 1244,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } "
},
{
"path": "components/ui/radio-group.tsx",
"chars": 1481,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\nimport {"
},
{
"path": "components/ui/select-position.tsx",
"chars": 1719,
"preview": "\"use client\"\nimport { cn } from \"@lib/utils\"\nimport { useState } from \"react\"\n\nexport const SelectBackgroundPosition = ("
},
{
"path": "components/ui/select.tsx",
"chars": 5629,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { Check, C"
},
{
"path": "components/ui/slider.tsx",
"chars": 1113,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SliderPrimitive from \"@radix-ui/react-slider\"\n\nimport { cn } fr"
},
{
"path": "components/ui/sonner.tsx",
"chars": 894,
"preview": "\"use client\"\n\nimport { useTheme } from \"next-themes\"\nimport { Toaster as Sonner } from \"sonner\"\n\ntype ToasterProps = Rea"
},
{
"path": "components/ui/switch.tsx",
"chars": 1164,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\"\n\nimport { cn } f"
},
{
"path": "components/ui/tabs.tsx",
"chars": 1897,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \""
},
{
"path": "components/ui/textarea.tsx",
"chars": 772,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface TextareaProps\n extends React.Textare"
},
{
"path": "components/ui/toast.tsx",
"chars": 4859,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ToastPrimitives from \"@radix-ui/react-toast\"\nimport { cva, type"
},
{
"path": "components/ui/toaster.tsx",
"chars": 794,
"preview": "\"use client\"\n\nimport {\n Toast,\n ToastClose,\n ToastDescription,\n ToastProvider,\n ToastTitle,\n ToastViewport,\n} from"
},
{
"path": "components/ui/use-toast.ts",
"chars": 3948,
"preview": "\"use client\"\n\n// Inspired by react-hot-toast library\nimport * as React from \"react\"\n\nimport type {\n ToastActionElement,"
},
{
"path": "components.json",
"chars": 343,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"default\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {\n"
},
{
"path": "lib/utils.ts",
"chars": 166,
"preview": "import { type ClassValue, clsx } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: Cla"
},
{
"path": "next-env.d.ts",
"chars": 201,
"preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
},
{
"path": "next.config.js",
"chars": 1594,
"preview": "// const isProd = process.env.NODE_ENV === 'production'\nconst isProd = false;\nconst bundleAnalyzer = require('@next/bund"
},
{
"path": "package.json",
"chars": 3583,
"preview": "{\n \"name\": \"X Cards Native Tweet Card service for X\",\n \"displayName\": \"Seamlessly integrate card services directly on "
},
{
"path": "popup.tsx",
"chars": 116,
"preview": "// export const Popup = () => {\n// return (\n// <div className=\"w-4 h-4\">\n// </div>\n// )\n// }"
},
{
"path": "postcss.config.js",
"chars": 203,
"preview": "/**\n * @type {import('postcss').ProcessOptions}\n */\nmodule.exports = {\n plugins: {\n \"postcss-import\": {},\n "
},
{
"path": "src/app/(app)/components/FrequentlyAskedQuestions.tsx",
"chars": 2527,
"preview": "\"use client\"\nimport { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from \"@components/ui/accordion\"\nimp"
},
{
"path": "src/app/(app)/components/GoogleFontSelector.tsx",
"chars": 3272,
"preview": "import React, { useState, useEffect } from 'react';\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectVal"
},
{
"path": "src/app/(app)/components/ImageLayout.tsx",
"chars": 4160,
"preview": "import { useCardStore } from '@src/hooks/useCardStore';\nimport React, { useEffect, useRef, useState } from 'react';\n\ncon"
},
{
"path": "src/app/(app)/components/LazyLoadAnimatedSection.tsx",
"chars": 1021,
"preview": "import { useInView } from 'react-intersection-observer';\nimport { motion} from \"framer-motion\"\nconst LazyLoadAnimatedSec"
},
{
"path": "src/app/(app)/components/ResultIcon.tsx",
"chars": 5481,
"preview": "import React, { useId } from \"react\";\n\n// import noisePicture from \"../assets/noise.inline.png\";\n\n// import { SettingsTy"
},
{
"path": "src/app/(app)/components/card-generator/color.tsx",
"chars": 6437,
"preview": "import { cn } from \"@lib/utils\";\nimport { ColorChangeHandler, SketchPicker } from \"react-color\";\nimport ResultIcon from "
},
{
"path": "src/app/(app)/components/card-generator/controller/background-controller.tsx",
"chars": 13481,
"preview": "import { AccordionContent, AccordionItem, AccordionTrigger } from \"@components/ui/accordion\";\nimport { ColorPicker } fro"
},
{
"path": "src/app/(app)/components/card-generator/controller/card-controller.tsx",
"chars": 7887,
"preview": "import { AccordionContent, AccordionItem, AccordionTrigger } from \"@components/ui/accordion\";\nimport { ColorPicker } fro"
},
{
"path": "src/app/(app)/components/card-generator/controller/font-controller.tsx",
"chars": 2220,
"preview": "import { AccordionContent, AccordionItem, AccordionTrigger } from \"@components/ui/accordion\";\nimport { Slider } from \"@c"
},
{
"path": "src/app/(app)/components/card-generator/controller/iframe-controller.tsx",
"chars": 10209,
"preview": " {/* <AccordionItem value={'frames'}>\n <AccordionTrigger>Frames</AccordionTrigger>\n "
},
{
"path": "src/app/(app)/components/card-generator/controller/input-controller.tsx",
"chars": 4433,
"preview": "import { AccordionContent, AccordionItem, AccordionTrigger } from \"@components/ui/accordion\";\nimport { Input } from \"@co"
},
{
"path": "src/app/(app)/components/card-generator/display.tsx",
"chars": 3212,
"preview": "import { CommonLayouts, useCardStore } from \"@src/hooks/useCardStore\"\nimport { useMemo } from \"react\"\n\n\nimport { presets"
},
{
"path": "src/app/(app)/components/card-generator/export-tab.tsx",
"chars": 1126,
"preview": "import { AccordionContent, AccordionItem, AccordionTrigger } from \"@components/ui/accordion\";\n\nimport { Button } from \"@"
},
{
"path": "src/app/(app)/components/card-generator/index.tsx",
"chars": 1545,
"preview": "import { Accordion, } from \"@components/ui/accordion\";\nimport { Display } from \"./display\";\nimport { useCardStore } from"
},
{
"path": "src/app/(app)/components/card-generator/twitter-card.tsx",
"chars": 9427,
"preview": "import React, { useMemo, useState } from 'react';\nimport { cn } from '@/lib/utils'; // Assuming you have a utility for c"
},
{
"path": "src/app/(app)/components/card-generator/wechat-card.tsx",
"chars": 6597,
"preview": "import type { CardStore, XConfig } from \"@src/hooks/useCardStore\"\n\ninterface WechatCardProps {\n xConfig: XConfig,\n "
},
{
"path": "src/app/(app)/components/dynamic-style-tippy.tsx",
"chars": 4329,
"preview": "import Tippy from \"@tippyjs/react\";\nimport { useEffect, useRef } from \"react\";\n\nexport const DynamicStyleTippyComponent "
},
{
"path": "src/app/(app)/components/hero.tsx",
"chars": 2796,
"preview": "\nimport chromeSvg from '@assets/chrome.svg';\nimport Image from \"next/image\";\n\nconst Hero = () => {\n return (\n "
},
{
"path": "src/app/(app)/components/save-as-template-button.tsx",
"chars": 1964,
"preview": "import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFoote"
},
{
"path": "src/app/(app)/components/sections/FeaturesGridSection.tsx",
"chars": 4647,
"preview": "// import yellowWaveDown from \"@/public/images/banner-wave.png\";\n// import Image from \"next/image\";\nimport { BookCopy, L"
},
{
"path": "src/app/(app)/components/sections/features2.tsx",
"chars": 3569,
"preview": "// import { Icon } from \"@/components/ui/icon\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@src/component"
},
{
"path": "src/app/(app)/components/sections/footer.tsx",
"chars": 2622,
"preview": "import Link from \"next/link\";\nimport Logo from \"@assets/icon.png\";\n\nexport const FooterSection = () => {\n return (\n "
},
{
"path": "src/app/(app)/components/sections/video.tsx",
"chars": 222,
"preview": "\"use client\"\nimport ReactPlayer from 'react-player'\n\n\nconst VideoSection = () => {\n return (<div className='mx-auto'>"
},
{
"path": "src/app/(app)/components/template-list.tsx",
"chars": 1949,
"preview": "import { Button } from \"@components/ui/button\";\nimport { useCardStore } from \"@src/hooks/useCardStore\";\nimport { useTemp"
},
{
"path": "src/app/(app)/components/x-form.tsx",
"chars": 1083,
"preview": "import { Button } from \"@components/ui/button\";\nimport { Input } from \"@components/ui/input\";\nimport { useCardStore, typ"
},
{
"path": "src/app/(app)/layout.tsx",
"chars": 16619,
"preview": "// \"use client\"\nimport Image from 'next/image';\nimport Logo from '@assets/icon.png';\n\nconst AppLayout = ({ children }) ="
},
{
"path": "src/app/(app)/page.tsx",
"chars": 2700,
"preview": "import * as _ from 'lodash-es';\nimport Hero from \"./components/hero\";\nimport { cn } from \"@lib/utils\";\nimport gradientBo"
},
{
"path": "src/app/(app)/request.tsx",
"chars": 0,
"preview": ""
},
{
"path": "src/app/(extension)/independent/components/index.tsx",
"chars": 4019,
"preview": "\"use client\"\nimport { useEffect, useRef } from \"react\";\nimport { useCardStore, type XConfig } from \"@src/hooks/useCardSt"
},
{
"path": "src/app/(extension)/independent/page.tsx",
"chars": 462,
"preview": "// import Index from \"./components\";\n\nexport async function generateStaticParams() {\n return [{ slug: 'independent' }"
},
{
"path": "src/app/(extension)/welcome/page.tsx",
"chars": 11592,
"preview": "import { cn } from \"@lib/utils\"\nimport gradientBottomSvg from '@assets/gradient-bottom.svg';\nimport introPng from '@asse"
},
{
"path": "src/app/api/license/route.ts",
"chars": 1827,
"preview": "import { NextResponse, type NextRequest } from \"next/server\";\nimport { createClient } from '@supabase/supabase-js'\n\nexpo"
},
{
"path": "src/app/api/x/route.ts",
"chars": 721,
"preview": "// import type { NextApiRequest } from \"next\";\n// import { NextResponse, type NextRequest } from \"next/server\";\n\n// // e"
},
{
"path": "src/app/globals.css",
"chars": 2445,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n :root {\n --background: 0 0% 100%;\n --f"
},
{
"path": "src/app/layout.tsx",
"chars": 2005,
"preview": "import { Toaster } from \"@/components/ui/sonner\"\nimport Logo from '@assets/icon.png'\nimport './globals.css'\nimport { sit"
},
{
"path": "src/app/utils/IFrameMessageSystem.ts",
"chars": 1807,
"preview": "type MessageCallback = (value: any) => void;\n\ninterface MessageEvent {\n data: {\n action: string;\n value: any;\n }"
},
{
"path": "src/app/utils/element.ts",
"chars": 1610,
"preview": "export function traverseAndCheck(element, condition) {\n if (condition(element)) return true;\n return Array.from(el"
},
{
"path": "src/app/utils/export.ts",
"chars": 4359,
"preview": "import type {XConfig} from \"@src/hooks/useCardStore\";\n\nimport JSZip from 'jszip';\nimport {saveAs} from 'file-saver';\n\n\ne"
},
{
"path": "src/app/utils/format.ts",
"chars": 670,
"preview": "export function formatTimestamp(timestamp: number): string {\n const date = new Date(timestamp * 1000); // Convert sec"
},
{
"path": "src/app/utils/image.ts",
"chars": 5116,
"preview": "import { domToPng, domToJpeg, domToSvg, type Options, createContext, destroyContext } from 'modern-screenshot'\n\n\n\n// con"
},
{
"path": "src/app/utils/index.ts",
"chars": 8498,
"preview": "import * as _ from 'lodash-es';\n\nimport { domToPng, domToJpeg, domToSvg, type Options, createContext, destroyContext } f"
},
{
"path": "src/background/index.ts",
"chars": 612,
"preview": "import { sendToContentScript } from \"@plasmohq/messaging\"\n\n\nchrome.runtime.onMessage.addListener((request, sender, sendR"
},
{
"path": "src/background/messages/code.ts",
"chars": 2269,
"preview": "import type { PlasmoMessaging } from \"@plasmohq/messaging\"\nimport * as _ from 'lodash-es'\nimport Browser from \"webextens"
},
{
"path": "src/background/messages/tweet.ts",
"chars": 825,
"preview": "import type { PlasmoMessaging } from \"@plasmohq/messaging\"\nimport * as _ from 'lodash-es'\n\n\nconst handler: PlasmoMessagi"
},
{
"path": "src/components/extension/card-button.tsx",
"chars": 7664,
"preview": "import { DynamicStyleTippyComponent } from \"@src/app/(app)/components/dynamic-style-tippy\";\nimport { useMemo, useRef, us"
},
{
"path": "src/components/extension/input-code.tsx",
"chars": 6587,
"preview": "import React, { useEffect, useState } from 'react';\nimport { X } from 'lucide-react';\nimport { motion } from 'framer-mot"
},
{
"path": "src/components/extension/label-with-icon.tsx",
"chars": 657,
"preview": "import React from 'react';\nimport { LockOpen, Lock } from 'lucide-react';\n\ninterface LabelWithIconProps {\n label: str"
},
{
"path": "src/components/extension/layout-options.tsx",
"chars": 2510,
"preview": "import { CommonLayouts } from '@src/hooks/useCardStore';\nimport React from 'react';\nimport styled from 'styled-component"
},
{
"path": "src/components/extension/preset-color-list.tsx",
"chars": 1658,
"preview": "import ResultIcon from \"@src/app/(app)/components/ResultIcon\"\nimport { presets } from \"@src/app/(app)/components/card-ge"
},
{
"path": "src/components/extension/tabs.tsx",
"chars": 1711,
"preview": "import React, { useEffect, useState } from 'react';\nimport { Lock } from 'lucide-react';\nimport { useTweetsStore } from "
},
{
"path": "src/components/extension/tweet-manager.tsx",
"chars": 3829,
"preview": "import type { XConfig } from '@src/hooks/useCardStore';\nimport { Trash } from 'lucide-react';\nimport React, { useEffect,"
},
{
"path": "src/components/extension/use-tweet-collection.ts",
"chars": 3561,
"preview": "import type { CardStore, XConfig } from \"@src/hooks/useCardStore\";\nimport { create } from \"zustand\";\nimport { fontSizeMa"
},
{
"path": "src/components/extension/x-cards-toast/font-control.tsx",
"chars": 4172,
"preview": "import { Button } from \"@components/ui/button\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport { useTwee"
},
{
"path": "src/components/extension/x-cards-toast/image-preview.tsx",
"chars": 5193,
"preview": "import React, { useState, useCallback, useRef, useEffect } from 'react';\nimport { ZoomIn, ZoomOut, Move, RotateCcw } fro"
},
{
"path": "src/components/extension/x-cards-toast/index.module.css",
"chars": 0,
"preview": ""
},
{
"path": "src/components/extension/x-cards-toast/index.tsx",
"chars": 16835,
"preview": "import * as _ from 'lodash-es';\nimport React, { useEffect, useMemo, useRef, useState } from 'react';\nimport { PresetColo"
},
{
"path": "src/components/extension/x-cards-toast/padding-control.tsx",
"chars": 1559,
"preview": "import { Button } from \"@components/ui/button\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport { useTwee"
},
{
"path": "src/components/extension/x-cards-toast/scale-control.tsx",
"chars": 1311,
"preview": "import React from 'react';\nimport { Tabs, TabsList, TabsTrigger } from '@components/ui/tabs'; // Assuming you're using R"
},
{
"path": "src/components/extension/x-cards-toast/tweet-control.tsx",
"chars": 2738,
"preview": "import React, { useState } from 'react';\nimport { Activity, Clock, Heart, Twitter, User } from 'lucide-react';\nimport { "
},
{
"path": "src/components/sortableList.tsx",
"chars": 5641,
"preview": "\"use client\"\n\n// npx shadcn-ui@latest add checkbox\n// npm i react-use-measure\nimport { type Dispatch, type ReactNode, t"
},
{
"path": "src/components/ui/BentoGrid.tsx",
"chars": 2301,
"preview": "import type { ReactNode } from \"react\";\nimport { ArrowRightIcon } from \"@radix-ui/react-icons\";\n\nimport { cn } from \"@/l"
},
{
"path": "src/components/ui/DotPattern.tsx",
"chars": 1053,
"preview": "import { useId } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface DotPatternProps {\n width?: any;\n height?:"
},
{
"path": "src/components/ui/acetenity-tabs.tsx",
"chars": 3895,
"preview": "\"use client\";\n\nimport { useState } from \"react\";\nimport { motion } from \"framer-motion\";\nimport { cn } from \"@lib/utils\""
},
{
"path": "src/components/ui/animated-list.tsx",
"chars": 1845,
"preview": "\"use client\";\n\nimport React, { type ReactElement, useEffect, useMemo, useState } from \"react\";\nimport { AnimatePresence,"
},
{
"path": "src/components/ui/animatedBeam.tsx",
"chars": 6019,
"preview": "\"use client\";\n\nimport { type RefObject, useEffect, useId, useState } from \"react\";\nimport { motion } from \"framer-motion"
},
{
"path": "src/components/ui/api-key-panel.tsx",
"chars": 7819,
"preview": "// import { Tabs } from \"@src/components/ui/acetenity-tabs\";\nimport { requestTestConnection } from \"@src/app/(app)/reque"
},
{
"path": "src/components/ui/bold-copy.tsx",
"chars": 1306,
"preview": "import { Tourney } from \"next/font/google\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst tourney = Tourney({\n subsets: ["
},
{
"path": "src/components/ui/burnIn.tsx",
"chars": 921,
"preview": "\"use client\";\n\nimport { motion } from \"framer-motion\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface BlurIntProps {\n wo"
},
{
"path": "src/components/ui/card.tsx",
"chars": 1877,
"preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst Card = React.forwardRef<\n HTMLDivElement,\n Rea"
},
{
"path": "src/components/ui/chart.tsx",
"chars": 10586,
"preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RechartsPrimitive from \"recharts\"\nimport type {\n NameType,\n P"
},
{
"path": "src/components/ui/fade-text.tsx",
"chars": 1611,
"preview": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { motion, type Variants } from \"framer-motion\";\n\ntype FadeTextPro"
},
{
"path": "src/components/ui/grid-pattern.tsx",
"chars": 1923,
"preview": "import { useId } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface GridPatternProps {\n width?: any;\n hei"
},
{
"path": "src/components/ui/grid.tsx",
"chars": 1290,
"preview": "interface GridProps {\n /**\n * Color of the grid\n */\n color?: string;\n\n /**\n * Size of the grid in p"
},
{
"path": "src/components/ui/icon.tsx",
"chars": 348,
"preview": "import { icons } from \"lucide-react\";\n\nexport const Icon = ({\n name,\n color,\n size,\n className,\n}: {\n nam"
},
{
"path": "src/components/ui/loading-spinner.tsx",
"chars": 597,
"preview": "import { cn } from \"@lib/utils\"\n\nexport const LoadingSpinner = ({ className, isLoading, text }) => {\n {\n retur"
},
{
"path": "src/components/ui/marquee.tsx",
"chars": 1444,
"preview": "import { cn } from \"@/lib/utils\";\n\ninterface MarqueeProps {\n className?: string;\n reverse?: boolean;\n pauseOnHo"
},
{
"path": "src/components/ui/scroll-based-velocity.tsx",
"chars": 3165,
"preview": "\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport {\n motion,\n useAnimationFrame,\n use"
},
{
"path": "src/components/ui/shimmer-button.tsx",
"chars": 3516,
"preview": "import React, { type CSSProperties } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface ShimmerButtonPro"
},
{
"path": "src/components/ui/text-generate-effect.tsx",
"chars": 1411,
"preview": "\"use client\";\nimport { useEffect } from \"react\";\nimport { motion, stagger, useAnimate } from \"framer-motion\";\nimport { c"
},
{
"path": "src/components/ui/underline-hover-text.tsx",
"chars": 1197,
"preview": "\"use client\";\nimport React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface UnderlineHoverTextProps {\n"
},
{
"path": "src/config/site.ts",
"chars": 1757,
"preview": "const baseSiteConfig = {\n name: \"X Cards\",\n description:\n \"Share X anywhere, any format. A Chrome extension"
},
{
"path": "src/contents/plasmo-overlay.css",
"chars": 60,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n"
},
{
"path": "src/contents/plasmo-overlay.tsx",
"chars": 5527,
"preview": "import React, { useEffect, useRef } from 'react'\nimport type { PlasmoCSConfig, PlasmoCSUIProps, PlasmoRender } from \"pla"
},
{
"path": "src/contents/x-home.tsx",
"chars": 2276,
"preview": "import cssText from \"data-text:@src/contents/x.css\"\n\nimport type { PlasmoCSConfig, PlasmoCSUIProps, PlasmoGetInlineAncho"
},
{
"path": "src/contents/x.css",
"chars": 1817,
"preview": "#plasmo-shadow-container {\n z-index: 10 !important;\n /* position: fixed !important; */\n height: 100%;\n}\n\n#plasmo-inli"
},
{
"path": "src/hooks/useCardStore.tsx",
"chars": 9314,
"preview": "import type { TweetControlState } from '@src/components/extension/use-tweet-collection';\nimport { create } from 'zustand"
},
{
"path": "src/hooks/useTemplatesStore.tsx",
"chars": 1619,
"preview": "import { create } from 'zustand'\nimport { persist, createJSONStorage, type StateStorage } from 'zustand/middleware'\nimpo"
},
{
"path": "src/lib/BlurGradientBg.module.js",
"chars": 84236,
"preview": "function t(t,e){(null==e||e>t.length)&&(e=t.length);for(var i=0,r=Array(e);i<e;i++)r[i]=t[i];return r}function e(t,e,i){"
},
{
"path": "src/sandbox.tsx",
"chars": 0,
"preview": ""
},
{
"path": "tailwind.config.ts",
"chars": 5521,
"preview": "import type { Config } from \"tailwindcss\";\nconst svgToDataUri = require(\"mini-svg-data-uri\");\nconst colors = require(\"ta"
},
{
"path": "tsconfig.json",
"chars": 406,
"preview": "{\n \"extends\": \"plasmo/templates/tsconfig.base\",\n \"exclude\": [\"node_modules\"],\n \"include\": [\n \".plasmo/index.d.ts\","
},
{
"path": "vercel.json",
"chars": 94,
"preview": "{\n \"functions\": {\n \"app/api/**/*\": {\n \"maxDuration\": 60\n }\n }\n}"
}
]
About this extraction
This page contains the full source code of the hzeyuan/x-cards GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 125 files (476.8 KB), approximately 133.8k tokens, and a symbol index with 164 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.